diff --git a/.gitignore b/.gitignore index e8aaef53..2818b990 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,13 @@ chroma_db/ # pulled in repos repos/ +# virtual environments +.venv/ +venv/ + +# logs +*.log + # secrets .env memory/.channel/ diff --git a/Autotests/mock/conftest.py b/Autotests/mock/conftest.py index 93166a2c..7f81f337 100644 --- a/Autotests/mock/conftest.py +++ b/Autotests/mock/conftest.py @@ -60,3 +60,13 @@ def comm(): yield server finally: server.stop(5) + + +# Answers and the frames they were served for do not outlive the test that +# registered them, so a frame left unfinished by one test cannot answer for +# the next one. +@pytest.fixture(autouse=True) +def reset_llm(request): + yield + if "llm" in request.fixturenames: + request.getfixturevalue("llm").reset(5) diff --git a/Autotests/mock/llm.py b/Autotests/mock/llm.py index cc3ec749..623de555 100644 --- a/Autotests/mock/llm.py +++ b/Autotests/mock/llm.py @@ -6,17 +6,49 @@ except ImportError: from rpc import Rpc, IPCClient, IPCServer from contextlib import contextmanager +import re import threading LLM_MOCK_PORT = 9765 +FRAME_SECTION_HEADER = "CURRENT_CONTEXT_FRAME_S_EXPR:" +FRAME_SECTION_END = "_newline_SKILLS:" +FRAME_COMPLETION_SKILLS = ("complete-goals-stm", "complete-goals-ltm", "clear-frame-junk") +FRAME_COMPLETION_ANSWER = '(complete-goals-stm "Answered by the mock, frame completed by the test harness.")' +FRAME_COMPLETION_ATTEMPTS = 2 +FRAME_ID = re.compile(r"\(frameID\s+([^)\s]+)\)") + + +def unescape(text): + return (text + .replace("_apostrophe_", "'") + .replace("_quote_", '"') + .replace("_newline_", "\n")) + + +def frame_section(prompt): + start = prompt.find(FRAME_SECTION_HEADER) + if start < 0: + return None + start += len(FRAME_SECTION_HEADER) + end = prompt.find(FRAME_SECTION_END, start) + return prompt[start:] if end < 0 else prompt[start:end] + + +def frame_id(section): + found = FRAME_ID.search(section) + return found.group(1) if found else "unknown-frame" + + class LlmMockAgent: def __init__(self, address): self._lock = threading.Lock() self._answers = {} + self._served = {} self._rpc = Rpc(IPCClient(address)) self._rpc.on_request('set_answer', lambda args: self.on_set_answer(args)) + self._rpc.on_request('reset', lambda args: self.on_reset(args)) self._rpc.on_request('ping', lambda args: self.on_ping(args)) self._rpc.start() @@ -28,57 +60,114 @@ def chat(self, content): if len(user) < 2: return "" + message = self._message(user[1]) + if message is not None: + answer = self._message_answer(message) + if answer: + print(f"[LlmMockAgent] Mock answers: {answer}") + return answer + + section = frame_section(user[0]) + if section is not None: + return self._frame_answer(section) + + if message is not None: + print(f"[LlmMockAgent] Mock doesn't have answer for: {message}") + return "" + + def _message(self, suffix): try: - body = eval(user[1])[1] - except SyntaxError: - return "" + return eval(suffix)[1] + except Exception: + return None + def _message_answer(self, body): # The agent escapes punctuation that would confuse its s-exp # parser ('->_apostrophe_, "->_quote_, \n->_newline_) before # the text reaches chat(). set_answer stores the literal # prompt key, so try the raw body first, then the normalized # form so prompts with quotes/apostrophes/newlines still match. - def normalize(text): - return (text - .replace("_apostrophe_", "'") - .replace("_quote_", '"') - .replace("_newline_", "\n")) - with self._lock: - answer = self._answers.get(body) or self._answers.get(normalize(body)) + answer = self._response(self._answers.get(body) or self._answers.get(unescape(body))) if answer: - print(f"[LlmMockAgent] Mock answers: {answer}") return answer # IRC may deliver multiple PRIVMSGs in one agent iteration; the # agent concatenates them with " | " between speakers. Split # and look up each fragment individually so a registered answer # is not missed when several messages arrive together. - fragments = body.split(" | ") - for fragment in fragments: + for fragment in body.split(" | "): if ": " not in fragment: continue prompt = fragment.split(": ", 1)[1] with self._lock: - a = self._answers.get(normalize(prompt)) or self._answers.get(prompt) - if a: - answer = a + found = self._answers.get(unescape(prompt)) or self._answers.get(prompt) + if found: + answer = self._response(found) + + return answer + + # With context frames the received message is no longer passed after + # the ":-:-:-:" delimiter: the delimiter carries a loop signal and the + # text becomes the current frame, projected into the prompt under + # CURRENT_CONTEXT_FRAME_S_EXPR. Matching against that section rather + # than the whole prompt keeps the mock honest: a message that was + # admitted but did not become current is not answered. + def _frame_answer(self, section): + current = unescape(section) + frame = frame_id(section) - if answer: - print(f"[LlmMockAgent] Mock answers: {answer}") - return answer - else: - print(f"[LlmMockAgent] Mock doesn't have answer for: {body}") - return "" + with self._lock: + match = None + for request, entry in self._answers.items(): + if request in current or unescape(request) in current: + match = (request, entry) + break + + if match is None: + print(f"[LlmMockAgent] Mock doesn't have answer for frame: {frame}") + return "" + + request, (response, complete_frame) = match + served = self._served.get((frame, request), 0) + self._served[(frame, request)] = served + 1 + + # A frame outlives the iteration that answered it, so the answer is + # served once. While the same frame keeps coming back the harness + # completes it instead, otherwise the next message is admitted but + # never becomes current and every later test fails with it. + if served == 0: + if complete_frame and not any(skill in response for skill in FRAME_COMPLETION_SKILLS): + response = f"{response} {FRAME_COMPLETION_ANSWER}" + print(f"[LlmMockAgent] Mock answers: {response}") + return response + + if complete_frame and served <= FRAME_COMPLETION_ATTEMPTS: + print(f"[LlmMockAgent] Frame {frame} is still current, completing it") + return FRAME_COMPLETION_ANSWER + + print(f"[LlmMockAgent] Frame {frame} was already answered") + return "" + + def _response(self, entry): + return entry[0] if entry else None def on_set_answer(self, args): with self._lock: request = args['request'] response = args['response'] + complete_frame = args.get('complete_frame', True) print(f'[LlmMockAgent] Mock request: "{request}" with response "{response}"') - self._answers[request] = response + self._answers[request] = (response, complete_frame) return True + def on_reset(self, args): + with self._lock: + self._answers.clear() + self._served.clear() + print('[LlmMockAgent] Mock answers cleared') + return True + def on_ping(self, args): print(f'[LlmMockAgent] Mock ping request processed') return True @@ -92,13 +181,21 @@ def __init__(self, address): def stop(self, timeout=None): self._rpc.stop(timeout) - def set_answer(self, request, response, timeout=10): - result = self._rpc.request('set_answer', { 'request': request, 'response': response }) + def set_answer(self, request, response, complete_frame=True, timeout=10): + result = self._rpc.request('set_answer', { 'request': request, 'response': response, + 'complete_frame': complete_frame }) if result.get(timeout) != True: print(f'[LlmMockController] Cannot set answer to the mock, error: {result.error()}') return False return True + def reset(self, timeout=10): + result = self._rpc.request('reset', {}) + if result.get(timeout) != True: + print(f'[LlmMockController] Cannot reset the mock, error: {result.error()}') + return False + return True + def ping(self, timeout=None): print(f'[LlmMockController] Ping agent') result = self._rpc.request('ping', {}) diff --git a/Autotests/mock/test_llm.py b/Autotests/mock/test_llm.py index e9300455..ebca33cc 100644 --- a/Autotests/mock/test_llm.py +++ b/Autotests/mock/test_llm.py @@ -5,6 +5,27 @@ TEST_ADDRESS = (LOCALHOST, 9767) +FRAME = "Frame-20260729T144803343500Z" +OTHER_FRAME = "Frame-20260729T144839020270Z" +SIGNAL = "NEW_INPUT_HAS_BEEN_ADMITTED_AS_ACTIVE_FRAME." + + +def frame_prompt(deliverable, frame=FRAME, signal=SIGNAL): + return ( + "PROMPT: You are a OmegaClaw agentic harness in a continuous loop._newline_" + "RUNTIME-PROMPT: The current frame is the authoritative task state._newline_" + "CURRENT_CONTEXT_FRAME_S_EXPR: (ContextProjection (RootFrame (RootFrame RootFrame-1 " + f"(current-frame-id {frame}) (last-admitted-frame-id {frame}) (mode Fast))) " + f"(CurrentFrame (Frame (frameID {frame}) (parent-frameID ()) (source UserDirective) " + "(priority 1.0) (status Active) (frame-mode Fast) " + f"(history-summary sha256:a48d311b2a4172dc chars:{len(deliverable)} excerpt:{deliverable}) " + f"(deliverables ({deliverable})) (results ()))))_newline_" + "SKILLS: - Remember a particular string: remember string_newline_" + "OUTPUT_FORMAT: Up to 5 lines_newline_" + "TIME: 2026-07-29 14:48:03" + f":-:-:-:{signal}" + ) + class TestLlmMock: def setup_class(cls): @@ -64,3 +85,111 @@ def test_context_manager_timeout(self, agent): assert False except RuntimeError as e: assert e.args == ("Agent didn't answered in 2 seconds",) + + def test_iteration_without_message_is_silent(self, agent, controller, capsys): + assert controller.set_answer("hello", "world") + capsys.readouterr() + + assert agent.chat("PROMPT: nothing new here:-:-:-:") == "" + + assert "Mock doesn't have answer" not in capsys.readouterr().out + + def test_missing_answer_is_reported_with_the_message(self, agent, controller, capsys): + assert controller.set_answer("hello", "world") + capsys.readouterr() + + assert agent.chat(":-:-:-:['HUMAN-MSG', 'test: goodbye']") == "" + + assert "Mock doesn't have answer for: test: goodbye" in capsys.readouterr().out + + def test_reset_drops_answers(self, agent, controller): + assert controller.set_answer("hello", "world") + assert agent.chat(":-:-:-:['HUMAN-MSG', 'test: hello']") == "world" + assert controller.reset() + assert agent.chat(":-:-:-:['HUMAN-MSG', 'test: hello']") == "" + + +class TestLlmMockContextFrames: + + @pytest.fixture + def agent(self): + agent = LlmMockAgent(TEST_ADDRESS) + yield agent + agent.stop(5) + + @pytest.fixture + def controller(self): + controller = LlmMockController(TEST_ADDRESS) + yield controller + controller.stop(5) + + def test_answer_matched_against_current_frame(self, agent, controller): + request = "[REQ-1] please write Hello into /tmp/hello.txt" + assert controller.set_answer(request, '(write-file "/tmp/hello.txt" "Hello")') + + answer = agent.chat(frame_prompt(request)) + + assert answer.startswith('(write-file "/tmp/hello.txt" "Hello")') + + def test_answer_completes_the_frame(self, agent, controller): + request = "[REQ-2] send Done" + assert controller.set_answer(request, '(send "Done")') + + answer = agent.chat(frame_prompt(request)) + + assert answer == f'(send "Done") {FRAME_COMPLETION_ANSWER}' + + def test_own_completion_is_not_duplicated(self, agent, controller): + request = "[REQ-3] send Done" + response = '(send "Done") (complete-goals-ltm "kept for later")' + assert controller.set_answer(request, response) + + assert agent.chat(frame_prompt(request)) == response + + def test_frame_kept_open_when_test_asks_for_it(self, agent, controller): + request = "[REQ-4] send Done" + assert controller.set_answer(request, '(send "Done")', complete_frame=False) + + assert agent.chat(frame_prompt(request)) == '(send "Done")' + + def test_answer_is_served_once_then_frame_is_drained(self, agent, controller): + request = "[REQ-5] send Done" + assert controller.set_answer(request, '(send "Done")') + prompt = frame_prompt(request) + + first = agent.chat(prompt) + second = agent.chat(prompt) + third = agent.chat(prompt) + fourth = agent.chat(prompt) + + assert first.startswith('(send "Done")') + assert second == FRAME_COMPLETION_ANSWER + assert third == FRAME_COMPLETION_ANSWER + assert fourth == "" + + def test_same_request_in_a_new_frame_is_answered_again(self, agent, controller): + request = "[REQ-6] send Done" + assert controller.set_answer(request, '(send "Done")') + + assert agent.chat(frame_prompt(request)).startswith('(send "Done")') + assert agent.chat(frame_prompt(request, frame=OTHER_FRAME)).startswith('(send "Done")') + + def test_escaped_request_matches(self, agent, controller): + request = 'don\'t write "Hello world" into\n/tmp/e.txt' + escaped = 'don_apostrophe_t write _quote_Hello world_quote_ into_newline_/tmp/e.txt' + assert controller.set_answer(request, '(send "ok")') + + assert agent.chat(frame_prompt(escaped)).startswith('(send "ok")') + + def test_unknown_frame_is_not_answered(self, agent, controller): + assert controller.set_answer("[REQ-7] something else", '(send "Done")') + + assert agent.chat(frame_prompt("[REQ-8] not registered")) == "" + + def test_request_outside_the_frame_section_is_not_answered(self, agent, controller): + request = "[REQ-9] send Done" + assert controller.set_answer(request, '(send "Done")') + prompt = frame_prompt("another task entirely") + prompt = prompt.replace("OUTPUT_FORMAT:", f"LAST_SKILL_USE_RESULTS: {request}_newline_OUTPUT_FORMAT:") + + assert agent.chat(prompt) == "" diff --git a/Dockerfile b/Dockerfile index db567b4a..57710143 100644 --- a/Dockerfile +++ b/Dockerfile @@ -128,4 +128,4 @@ RUN cp ${OMEGACLAW_DIR}/run.metta /PeTTa/run.metta \ && chown -R 65534:65534 /opt/huggingface /opt/sentence_transformers ENTRYPOINT ["/PeTTa/repos/OmegaClaw-Core/entrypoint.sh"] -CMD [] +CMD [] \ No newline at end of file diff --git a/channels/auth.py b/channels/auth.py index e4e77a91..3720502a 100644 --- a/channels/auth.py +++ b/channels/auth.py @@ -22,7 +22,8 @@ def get_proxy_url(): global _proxy_url if _proxy_url is None: - _proxy_url = config_get_by_key("GATEWAY_URL", "").rstrip("/") + configured_url = config_get_by_key("GATEWAY_URL", "") + _proxy_url = str(configured_url or "").strip().rstrip("/") return _proxy_url diff --git a/lib_nal.metta b/lib_nal.metta index 714f36a7..db4379fe 100644 --- a/lib_nal.metta +++ b/lib_nal.metta @@ -9,7 +9,7 @@ (stv $f2 $c2)) (stv (* $f1 $f2) (* (* $f1 $f2) (* $c1 $c2)))) -(= (Truth_Abduction (stv $f1 $c1) +(= (Truth_Abduction (stv $f1 $c1) (stv $f2 $c2)) (stv $f2 (Truth_w2c (* (* $f1 $c1) $c2)))) @@ -22,14 +22,14 @@ (= (Truth_StructuralDeduction $T) (Truth_Deduction $T (stv 1.0 0.9))) - + (= (Truth_Negation (stv $f $c)) (stv (- 1 $f) $c)) (= (Truth_StructuralDeductionNegated $T) (Truth_Negation (Truth_StructuralDeduction $T))) -(= (Truth_Intersection (stv $f1 $c1) +(= (Truth_Intersection (stv $f1 $c1) (stv $f2 $c2)) (stv (* $f1 $f2) (* $c1 $c2))) @@ -41,12 +41,12 @@ (= (Truth_Comparison (stv $f1 $c1) (stv $f2 $c2)) - (let $f0 (Truth_or $f1 $f2) + (let $f0 (Truth_or $f1 $f2) (stv (if (== $f0 0.0) - 0.0 + 0.0 (/ (* $f1 $f2) $f0)) (Truth_w2c (* $f0 (* $c1 $c2)))))) - + (= (Truth_Analogy (stv $f1 $c1) (stv $f2 $c2)) (stv (* $f1 $f2) (* (* $c1 $c2) $f2))) @@ -199,4 +199,4 @@ (= (|-nal ($B $T1) ((==> $A (¬ $B)) $T2)) ($A (Truth_Abduction (Truth_Negation $T1) $T2))) (= (|- $a $b) - (unique-atom (collapse (superpose ((|-nal $a $b) (|-nal $b $a)))))) + (unique-atom (collapse (superpose ((|-nal $a $b) (|-nal $b $a)))))) \ No newline at end of file diff --git a/lib_omegaclaw.metta b/lib_omegaclaw.metta index ea9636bf..66627a62 100644 --- a/lib_omegaclaw.metta +++ b/lib_omegaclaw.metta @@ -24,6 +24,7 @@ !(import! &self (library OmegaClaw-Core ./src/skills)) !(import! &self (library OmegaClaw-Core ./src/websearch.py)) !(import! &self (library OmegaClaw-Core ./src/memory)) +!(import! &self (library OmegaClaw-Core ./src/frame_relation.py)) !(import! &self (library OmegaClaw-Core ./src/context)) !(import! &self (library OmegaClaw-Core ./src/loop)) !(import! &self (library OmegaClaw-Core ./src/rag.py)) diff --git a/memory/prompt.txt b/memory/prompt.txt index 57920b65..12fa93c4 100644 --- a/memory/prompt.txt +++ b/memory/prompt.txt @@ -6,4 +6,4 @@ Only use pin for task state, and remember for items that could be valuable in th Assume long-term memory holds required information, ALWAYS query before responding anything! Take at least 5 agent cycles with extensive queries, pinning relevant items, before answering a new message or making decision. If you see command errors, please fix the format and re-invoke one-by-one. Do not use quote but a real quote in commands. -Responses must be short, communicate with purpose. +Responses must be short, communicate with purpose. \ No newline at end of file diff --git a/memory/prompt_context_frame.txt b/memory/prompt_context_frame.txt new file mode 100644 index 00000000..e586f1d0 --- /dev/null +++ b/memory/prompt_context_frame.txt @@ -0,0 +1,26 @@ +CONTEXT FRAME POLICY + +CURRENT_CONTEXT_FRAME_S_EXPR is the authoritative compact state for this cycle. +Use its goals, source, status, mode, constraints, deliverables, history summary, +and results to decide the next action. + +Serve UserDirective frames before AgentDirective frames. New user work and its +related results or errors take priority over all autonomous work. Do not start or +continue autonomous work while user-directed work remains active. + +Answer simple requests directly. When a user request is finished, send the final +answer and invoke exactly one completion command in the same batch: +complete-goals-stm for ordinary completion, or complete-goals-ltm only when the +summary is durable and reusable. Never store raw tool output in long-term memory. + +Send only for new user input, relevant results or events, an unrepeated real +error, or an explicit user-facing deliverable. Do not send during idle inspection +or autonomous goal creation. + +When no user-directed frame is pending, continue an active AgentDirective frame, +create at most one useful low-priority autonomous frame, or take no action. Never +ask the user to choose or manage the agent's autonomous agenda unless requested. + +Use frame-management skills to update, inspect, switch, compact, or complete +frames. Use query or episodes only when required information is outside the +current frame. diff --git a/src/context.metta b/src/context.metta new file mode 100644 index 00000000..f1b41c50 --- /dev/null +++ b/src/context.metta @@ -0,0 +1,890 @@ +;; TODO: Frame composition is now based on semantic search and LLM classification, +;; for the future development it should avoid LLM classification and just +;; use symbolic reasoning, of course there is an issue of not enough dataset, +;; try to explore factor graph based reasoning of PLN/NAL it seems like a good approach + +(= (cfv2ModeFast) Fast) +(= (cfv2ModeSlow) Slow) + +(= (cfv2StatusActive) Active) +(= (cfv2StatusFocused) Focused) +(= (cfv2StatusSuspended) Suspended) +(= (cfv2StatusBlocked) Blocked) +(= (cfv2StatusCompleted) Completed) +(= (cfv2StatusFailed) Failed) +(= (cfv2StatusArchived) Archived) + +(= (cfv2GoalStatusProposed) Proposed) +(= (cfv2GoalStatusActive) Active) +(= (cfv2GoalStatusBlocked) Blocked) +(= (cfv2GoalStatusSatisfied) Satisfied) +(= (cfv2GoalStatusFailed) Failed) +(= (cfv2GoalStatusSuspended) Suspended) +(= (cfv2GoalStatusRejected) Rejected) + +(= (cfv2SourceUser) UserDirective) +(= (cfv2SourceAgent) AgentDirective) + +(= (cfv2SpaceActive) Active) +(= (cfv2SpaceCompleted) Completed) + +(= (cfv2PayloadLimit) 900) +(= (cfv2HistorySummaryLimit) 2400) +(= (cfv2LastResultsLimit) 1200) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Python helper wrappers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(= (cfv2-now) + (swrite (py-call (helper.cfv2_now)))) + +(= (cfv2-make-id $prefix) + (py-call (helper.make_id (repr $prefix)))) + +(= (cfv2-compact-plain $value) + (py-call + (helper.compact_plain + (repr $value) + (cfv2LastResultsLimit)))) + +(= (cfv2-compact-limited $value $limit) + (py-call + (helper.compact_plain + (repr $value) + $limit))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Defaults +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(= (cfv2-default-method) + (CertifiedMethod + (status None) + (description "") + (parameters ()) + (evaluation-protocol ()) + (certificate-hash ""))) + +(= (cfv2-default-budget) + (ResourceBudget + (max-output-tokens (maxOutputToken)) + (max-command-lines 5) + (wake-interval (wakeupInterval)) + (status Open))) + +(= (cfv2-default-constraints) + ((Constraint FrameIsAuthority + "The current frame is the authoritative task state for the focused work item.") + (Constraint RootIsPointerOnly + "RootFrame stores only IDs, space names, global budget and global constraints; full frames live in spaces.") + (Constraint NoRawThoughtPrompting + "Do not rely on raw transcript history as working state.") + (Constraint SkillCommandsOnly + "The agent may invoke only commands listed in the skill set.") + (Constraint AuditViaHistory + "Exact chronological audit is available through history.metta, pin, episodes and runtime logs; frames store compact history summaries.") + (Constraint NoUnjustifiedAutonomousSideEffects + "Autonomous actions must serve an active current frame and respect budget."))) + +(= (cfv2-default-modules) + (NONE FOR NOW + ; (Entry send + ; (ModuleProfile (kind Skill) (capabilities (RespondToUser)) (rating 1.0))) + )) + +(= (cfv2-prompt-modules) + ((Entry skill-set + (ModuleProfile + (kind SkillSet) + (capabilities + (send remember query episodes pin read-file write-file append-file websearch tavily-search technical-analysis + metta new-frame new-autonomous-frame switch-frame switch-mode show-root-frame show-current-frame show-frame-index + show-active-framespace show-completed-framespace cfv2-get-relation complete-goals-stm complete-goals-ltm clear-frame-junk)) + (rating 1.0))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Constructors +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(= (cfv2ActiveFrameSpace) + (get-state &cfv2-active-framespace)) +(= (cfv2CompletedFrameSpace) + (get-state &cfv2-completed-framespace)) +(= (cfv2FrameIndexSpace) + (get-state &cfv2-frame-indexspace)) + +;; TODO: rename function to -> cfv2-root-frame +;; - change the current-frame-id getter to this function cfv2-root-current-frame-id +;; Update: both comments are addressed +(= (cfv2-root-frame) + (RootFrame + (id (get-state &cfv2-root-id)) + (current-frame-id (cfv2-root-current-frame-id)) + (last-admitted-frame-id (get-state &cfv2-last-admitted-frame-id)) + (active-framespace &cfv2-active-framespace) + (completed-framespace &cfv2-completed-framespace) + (frame-indexspace &cfv2-frame-indexspace) + (relational-space &cfv2-relational-space) + (mode (get-state &cfv2-root-mode)) + (global-budget (get-state &cfv2-global-budget)) + (global-constraints (get-state &cfv2-global-constraints)))) + +(= (cfv2-make-goal $goalID $frameID $subFrameID $description $source $priority $criteria) + (Goal + (goalID $goalID) + (frameID $frameID) + (sub-frameID $subFrameID) + (description $description) + (status Active) + (source $source) + (priority $priority) + (dependencies ()) + (success-criteria $criteria) + (created-at (cfv2-now)) + (completed-at ()) + (completion-summary ()))) + +(= (cfv2-make-deliverable $description) + (Deliverable + (id (cfv2-make-id Deliverable)) + (description $description) + (status Pending))) + + +;; TODO: change the current-frame-id getter to this function cfv2-root-current-frame-id. +;; Also change how current frame is set. +(= (cfv2-current-frame) + (if (== (cfv2-root-current-frame-id) ()) + NO-CURRENT-FRAME-SET + ; (cfv2-get-frame (cfv2-root-current-frame-id) Active) + (Frame + (frameID (cfv2-root-current-frame-id)) + (parent-frameID (get-state &cfv2-current-parent-frame-id)) + (source (get-state &cfv2-current-source)) + (priority (get-state &cfv2-current-priority)) + (goal-namespace (get-state &cfv2-current-goal-namespace)) + (status (get-state &cfv2-current-status)) + (frame-mode (get-state &cfv2-current-frame-mode)) + (hypotheses (get-state &cfv2-current-hypotheses)) + (method (get-state &cfv2-current-method)) + (history-summary (get-state &cfv2-current-history-summary)) + (modules (get-state &cfv2-current-modules)) + (budget (get-state &cfv2-current-budget)) + (constraints (get-state &cfv2-current-constraints)) + (deliverables (get-state &cfv2-current-deliverables)) + (sub-frame-namespace (get-state &cfv2-current-sub-frame-namespace)) + (results (get-state &cfv2-current-results)) + (created-at (get-state &cfv2-current-created-at)) + (updated-at (get-state &cfv2-current-updated-at)) + (completed-timestamp (get-state &cfv2-current-completed-timestamp)) + (completed-summary (get-state &cfv2-current-completed-summary))) + )) + +(= (frame-for-prompt) + (if (== (cfv2-root-current-frame-id) ()) NO-CURRENT-FRAME-SET + (let* (((Frame $frameID $parentFrameID $source $priority $goalNamespace $status $frameMode $hypotheses $method $historySummary + $modules $budget $constraints $deliverables $subFrameNamespace $results $createdAt $updatedAt $completedTimestamp $completedSummary) + (cfv2-current-frame)) + ; ($_ (log DEBUG "context" ("Current frame for prompt: " $frameID))) ;; enable for debug + ) + + (Frame $frameID $parentFrameID $source $priority $goalNamespace + $status $frameMode $historySummary $deliverables $subFrameNamespace + $results $createdAt $updatedAt $completedTimestamp $completedSummary)))) + +;; TODO: change the current-frame-id getter to this function cfv2-root-current-frame-id. +;; Update: the current frame ID is now taken from the root's current frame ID. +(= (cfv2-make-frame-ref-from-current $frameID $space) + (let $currentFrameID (if (== () $frameID) (cfv2-root-current-frame-id) $frameID) + (let* + ( + ; ($_ (log DEBUG "context" $frame)) + ((Frame $frameID' $parentFrameID $source $priority $goalNamespace (status $statusValue) $frameMode $hypotheses $method (history-summary $historySummary) + $modules $budget $constraints $deliverables $subFrameNamespace $results $createdAt $updatedAt $completedTimestamp $completedSummary) + (cfv2-get-frame $currentFrameID $space)) + ; ($_ (log DEBUG "context" ("Creating frame reference for frameID: " $currentFrameID " in space: " $space))) + ) + (FrameRef + (frameID $currentFrameID) $parentFrameID (space (if (== $space &cfv2-completed-framespace) Completed Active)) $source + $priority (status $statusValue) $frameMode + (summary (cfv2-compact-limited $historySummary (cfv2PayloadLimit))) + $createdAt $updatedAt $completedTimestamp)))) + + +;; TODO: wrong implementation +;; Update: The function now returns a ContextProjection object with the root frame only. +(= (cfv2-context-projection) + (ContextProjection + (RootFrame (cfv2-root-frame)) + (CurrentFrame (frame-for-prompt)) + )) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Initialization +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(= (cfv2-clear-current-frame-cache) + (progn + (change-state! &cfv2-current-frame-id ()) + (change-state! &cfv2-current-frame-id ()) + (change-state! &cfv2-current-parent-frame-id ()) + (change-state! &cfv2-current-source ()) + (change-state! &cfv2-current-priority 0.0) + (change-state! &cfv2-current-goal-namespace ()) + (change-state! &cfv2-current-status ()) + (change-state! &cfv2-current-frame-mode ()) + (change-state! &cfv2-current-hypotheses ()) + (change-state! &cfv2-current-method (cfv2-default-method)) + (change-state! &cfv2-current-history-summary "") + (change-state! &cfv2-current-modules (cfv2-default-modules)) ;; TODO: just call compact skill from skill.metta + (change-state! &cfv2-current-budget (cfv2-default-budget)) + (change-state! &cfv2-current-constraints (cfv2-default-constraints)) + (change-state! &cfv2-current-deliverables ()) + (change-state! &cfv2-current-sub-frame-namespace ()) + (change-state! &cfv2-current-results ()) + (change-state! &cfv2-current-created-at ()) + (change-state! &cfv2-current-updated-at ()) + (change-state! &cfv2-current-completed-timestamp ()) + (change-state! &cfv2-current-completed-summary ()) + ; (change-state! &cfv2-current-goal ()) + CURRENT-FRAME-CACHE-CLEARED)) + +(= (cfv2-init-context-frames) + (progn + ;; Root state. + (cfv2-clear-current-frame-cache) + + (change-state! &cfv2-root-id (cfv2-make-id RootFrame)) + (change-state! &cfv2-root-mode Slow) + (change-state! &cfv2-global-budget (cfv2-default-budget)) + (change-state! &cfv2-global-constraints (cfv2-default-constraints)) + (change-state! &cfv2-last-admitted-frame-id ()) + (change-state! &cfv2-current-frame-id ()) + + ;; List-backed external spaces. These are intentionally not in RootFrame. + (change-state! &cfv2-active-framespace ()) + (change-state! &cfv2-completed-framespace ()) + (change-state! &cfv2-frame-indexspace ()) + (change-state! &cfv2-goalspace ()) + (change-state! &cfv2-subframespace ()) + + ;; Relational states used in frame composition + (change-state! &cfv2-relational-space ()) + + ;; Hot focused frame cache. + CONTEXT-FRAMES-V2-INITIALIZED)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Space append/index helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TODO: The function calls get-state regardless of the space being empty. +;; - Check the rest Space Append functions for the same issue. +(= (cfv2-add-active-frame $frame) + (let $currentSpace (if (== (get-state &cfv2-active-framespace) ()) + ((get-state &cfv2-active-framespace)) (get-state &cfv2-active-framespace)) + (change-state! &cfv2-active-framespace + (append $currentSpace ($frame))))) + +(= (cfv2-add-completed-frame $frame) + (let $currentSpace (if (== (get-state &cfv2-completed-framespace) ()) + ((get-state &cfv2-completed-framespace)) (get-state &cfv2-completed-framespace)) + (change-state! &cfv2-completed-framespace + (append $currentSpace ($frame))))) + +(= (cfv2-add-frame-ref $ref) + (let $currentSpace (if (== (get-state &cfv2-frame-indexspace) ()) + ((get-state &cfv2-frame-indexspace)) (get-state &cfv2-frame-indexspace)) + (change-state! &cfv2-frame-indexspace + (append $currentSpace ($ref))))) + +(= (cfv2-add-goal $goal $goalspace) + (let $currentSpace (if (== (get-state $goalspace) ()) + ((get-state $goalspace)) (get-state $goalspace)) + (change-state! $goalspace + (append $currentSpace ($goal))))) + +(= (cfv2-add-sub-frame $subFrame $subframespace) + (let $currentSpace (if (== (get-state $subframespace) ()) + ((get-state $subframespace)) (get-state $subframespace)) + (change-state! $subframespace + (append $currentSpace ($subFrame))))) + +(= (cfv2-add-and-remove $frameID $newFrame $space) + (let $newSpaceContent (filter-atom (get-state $space) $frame + (== (cfv2-check-frame-id $frame $frameID) False)) + (change-state! $space (append $newSpaceContent ($newFrame))))) + +;; TODO: change the way current frame is snapshotted. +;; Update: The snapshot function has changed to take a frame as an argument. The current frame is now snapshotted by passing a frame. +(= (cfv2-snapshot-current-frame $space $frame) + (if (== $space Completed) + (cfv2-add-completed-frame $frame) + (cfv2-add-active-frame $frame))) + +(= (cfv2-index-current-frame $frameID $space) + ; (if (== (cfv2-root-current-frame-id) ()) + ; NO-CURRENT-FRAME-TO-INDEX + (let $namespace (if (== $space Completed) &cfv2-completed-framespace &cfv2-active-framespace) + (cfv2-add-frame-ref (cfv2-make-frame-ref-from-current $frameID $namespace)))) + ; ) + +(= (cfv2-switch-mode) + (change-state! &cfv2-root-mode + (if (== (get-state &cfv2-root-mode) Fast) Slow Fast))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Root helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(= (cfv2-root-mode) + (get-state &cfv2-root-mode)) + +(= (cfv2-set-root-mode $mode) + (if (or (== $mode Fast) (== $mode Slow)) + (progn (change-state! &cfv2-root-mode $mode) (cfv2-root-frame)) + (InvalidRootMode $mode))) + +(= (cfv2-root-current-frame-id) + (get-state &cfv2-current-frame-id)) + +(= (cfv2-root-set-current-frame-id $frameID) + (change-state! &cfv2-current-frame-id $frameID)) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Current frame mutation helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(= (cfv2-touch-current-frame) + (change-state! &cfv2-current-updated-at (cfv2-now))) + +;; TODO: limit the size of the history summary to a certain number of characters or tokens +;; take in the frame then deconstruct the frame then change the state of that frame. +;; Update: Since the current frame cache has been set the history summary can be updated directly in the state without needing to deconstruct the frame. +(= (cfv2-update-current-history-summary $kind $summary) + (let $new-summary + (swrite ((get-state &cfv2-current-history-summary) | $kind : $summary)) + (change-state! &cfv2-current-history-summary $new-summary))) + +;; TODO: The snapshot function has changed to take a frame as an argument. +(= (cfv2-record-frame-note $kind $payload) + (if (== (get-state &cfv2-current-frame-id) ()) + NO-CURRENT-FRAME-FOR-COMMAND-BATCH + (let $summary (cfv2-compact-plain $payload) + (progn + (cfv2-update-current-history-summary $kind $summary) + (cfv2-touch-current-frame) + (FRAME-NOTE-RECORDED))))) + +(= (cfv2-record-command-batch $commands $results) + (if (== (get-state &cfv2-current-frame-id) ()) + NO-CURRENT-FRAME-FOR-COMMAND-BATCH + (let $resultsSummary (cfv2-compact-plain (append $commands $results)) + (progn + ; (cfv2-update-current-history-summary CommandBatch $resultsSummary) + (cfv2-touch-current-frame) + (HISTORY-SUMMARY-UPDATED))))) + +(= (cfv2-add-hypothesis $id $hypothesis) + (progn + (change-state! &cfv2-current-hypotheses + (append (get-state &cfv2-current-hypotheses) + ((Entry $id $hypothesis)))) + (cfv2-record-frame-note HypothesisAdded (Entry $id $hypothesis)))) + +;; TODO: Review required after full integration, On Entry $variant $metrics. +(= (cfv2-add-result $variant $metrics) + (progn + (change-state! &cfv2-current-results + (append (get-state &cfv2-current-results) + ((Entry $variant $metrics)))) + (cfv2-record-frame-note ResultAdded (Entry $variant $metrics)))) + +(= (cfv2-set-certified-method $description $parameters $evalProtocol $hash) + (progn + (change-state! &cfv2-current-method + (CertifiedMethod + (status Certified) + (description $description) + (parameters $parameters) + (evaluation-protocol $evalProtocol) + (certificate-hash $hash))) + (cfv2-record-frame-note CertifiedMethodUpdated (get-state &cfv2-current-method)))) + + +(= (cfv2-add-deliverable $description) + (progn + ; (log DEBUG "context" (get-state &cfv2-current-deliverables)) + (change-state! &cfv2-current-deliverables + (append + (get-state &cfv2-current-deliverables) + ((cfv2-make-deliverable $description)))) + ; (log DEBUG "context" "Changed current deliverables state") + (cfv2-record-frame-note DeliverableAdded $description) + )) + +(= (cfv2-add-constraint $constraint) + (progn + (change-state! &cfv2-current-constraints + (append (get-state &cfv2-current-constraints) + ($constraint))) + (cfv2-record-frame-note ConstraintAdded $constraint))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Frame creation and user-message ingestion +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TODO: change the default method, modules, constraints, budget, etc; they are bloating the frame. +(= (cfv2-create-frame $source $description $priority $mode) + (let* (($frameID (cfv2-make-id Frame)) + ($goalSpaceID (cfv2-make-id &GoalSpace)) + ($subFrameSpaceID (cfv2-make-id &SubFrameSpace)) + ($now (cfv2-now)) + ($deliverables $description) + ($subframe (cfv2-create-sub-frame $frameID $subFrameSpaceID $goalSpaceID $description $source $priority $deliverables)) + ($compFrame (Frame (frameID $frameID) (parent-frameID ()) (source $source) (priority $priority) (status Active) (frame-mode $mode) (deliverables ($deliverables)) (results ()))) + ($relation (cfv2-compose-relations $frameID $compFrame)) + ($_ (change-state! &cfv2-relational-space (append (get-state &cfv2-relational-space) $relation))) + ($frame (Frame + (frameID $frameID) + (parent-frameID ()) + (source $source) + (priority $priority) ;; For future their should be heuristic to determine the priority of a frame based on the priority of its goal. + (goal-namespace $goalSpaceID) + (status Active) + (frame-mode $mode) + (hypotheses ()) + (method (cfv2-default-method)) + (history-summary (cfv2-compact-limited $description (cfv2HistorySummaryLimit))) + (modules (getFrameSkillsCompact)) + (budget (cfv2-default-budget)) + (constraints ()) + (deliverables ($deliverables)) + (sub-frame-namespace $subFrameSpaceID) + (results ()) + (created-at $now) + (updated-at $now) + (completed-timestamp ()) + (completed-summary ()) + )) + ) + + (progn + (cfv2-snapshot-current-frame Active $frame) + ; (log DEBUG "context" (CREATING-FRAME $frameID FROM $source WITH-MODE $mode)) + (cfv2-index-current-frame $frameID Active) + ; (log DEBUG "context" (CREATING-FRAME $frameID FROM $source WITH-MODE $mode)) + (change-state! &cfv2-last-admitted-frame-id $frameID) + (FRAME-CREATED-AND-STORED-IN-ACTIVE-FRAME-SPACE) + + (if (and (== $source UserDirective) (== (cfv2-root-mode) Slow)) + (progn (switch-mode) (cfv2-load-frame $frameID) "NEW USER DIRECTED FRAME CREATED AND SWITCHED TO FAST MODE") + _) + + (NewframeID $frameID) + ))) + +;; User's message should always be on the Fast loop. +(= (cfv2-create-frame-from-user-message $msg) + (cfv2-create-frame UserDirective $msg 1.0 Fast)) + +(= (cfv2-create-autonomous-frame $description $priority $mode) + (cfv2-create-frame AgentDirective $description $priority $mode)) + +(= (cfv2-record-message-for-current-frame $msg) + (if (== (get-state &cfv2-current-frame-id) ()) + NO-CURRENT-FRAME-FOR-COMMAND-BATCH + (progn + (cfv2-update-current-history-summary UserMessageForCurrentFrame (cfv2-compact-plain $msg)) + (cfv2-touch-current-frame) + MESSAGE-RECORDED-FOR-CURRENT-FRAME + ))) + +;; TODO: There should be another function to update a frame with a user message if there is a frame already consisting +;; ongoing frame for that specific user/module/sub-agents. +;; Update: Frame composition is implemented to address the above issue. +(= (cfv2-ingest-user-message $msg) + (cfv2-create-frame-from-user-message $msg)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Frame Composition +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(= (cfv2-relation-types) (DuplicateOf ContinuationOf FollowUp SubgoalOf + ParentOf DependsOn Blocks Supersedes SameProject + SameFailureCluster RelatedButSeparate Unrelated)) + +;; TODO: Add a hyperparameter config since K is set to 5 by default. +(= (cfv2-compose-relations $queryFrameID $compFrame) + (sread + (py-call + (frame_relation.cfv2_compose_frame_relations + (repr ($compFrame)) + (repr $queryFrameID) + (repr (cfv2-relation-types)) + (repr (embeddingprovider)) + 5)))) + +;; To be used as a tool for the agent. +(= (cfv2-get-relation $currentframeID) + (filter-atom (get-state &cfv2-relational-space) (Relation (FrameID-1 $a) (FrameID-2 $b) (Class $class) (Reason $reason) (Confidence $conf)) + (or (== $currentframeID $a) (== $currentframeID $b)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SubFrame creation +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(= (cfv2-create-sub-frame $frameID $subFrameSpaceID $goalSpace $description $source $priority $deliverables) + (let* (($subFrameID (cfv2-make-id SubFrame)) + ($goalID (cfv2-make-id Goal)) + ($goal (cfv2-make-goal $goalID $frameID $subFrameID + $description $source $priority $deliverables)) + ($subFrame + (SubFrame + (sub-frameID $subFrameID) + (frameID $frameID) + (priority $priority) + (status Active) + (goal $goal) + (deliverables $deliverables) + (dependencies ()) + (created-at (cfv2-now)) + (completed-at ()) + (completion-summary ())))) + (progn + ; (log DEBUG "context" $goalspace) + (change-state! $goalSpace ()) + (change-state! $subFrameSpaceID ()) + (cfv2-add-goal $goal $goalSpace) + ; (log DEBUG "context" $goalSpace) + (cfv2-add-sub-frame $subFrame $subFrameSpaceID) + ; (log DEBUG "context" $subFrameSpaceID) + $subFrame))) + +(= (cfv2-complete-sub-frame $subFrameID $summary) + (cfv2-record-frame-note SubFrameCompleted + (SubFrameCompletion + (sub-frameID $subFrameID) + (summary $summary) + (completed-at (cfv2-now))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Frame retrieval and switching +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Alternative for cfv2-load-frame-atom and cfv2-switch-frame +;; TODO: when switching frames, the current frame should be snapshotted and indexed before switching to the new frame. +;; Update: the current frame is now snapshoted and relational composition is done before switching to the new frame. +(= (cfv2-load-frame $currentframeID) + (let* ( + ($frameIDAtom (if (== (get-metatype $currentframeID) Grounded) (sread $currentframeID) $currentframeID)) + ((Frame (frameID $frameID) (parent-frameID $parentID) (source $source) + (priority $priority) (goal-namespace $goalSpace) (status $status) + (frame-mode $mode) (hypotheses $hypotheses) (method $methods) + (history-summary $historySummary) (modules $modules) (budget $budget) + (constraints $constraint) (deliverables $deliverables) + (sub-frame-namespace $subFrameSpaceID) (results $result) + (created-at $createdAt) (updated-at $updatedAt) (completed-timestamp $completedTimestamp) + (completed-summary $completedSummary)) + + (cfv2-get-frame $frameIDAtom &cfv2-active-framespace)) + + ($relation (collapse (cfv2-get-relation $frameIDAtom))) + ) + + (if (== $mode (cfv2-root-mode)) + (progn + ;; Safe offloading frames back to active space when switching frames before they are completed. + (if (== () (get-state &cfv2-current-frame-id)) NEW-FRAME-SWITCHING (cfv2-add-and-remove (get-state &cfv2-current-frame-id) (cfv2-current-frame) &cfv2-active-framespace)) + + (change-state! &cfv2-current-frame-id $frameIDAtom) + (change-state! &cfv2-current-parent-frame-id $parentID) + (change-state! &cfv2-current-source $source) + (change-state! &cfv2-current-priority $priority) + (change-state! &cfv2-current-goal-namespace $goalSpace) + (change-state! &cfv2-current-status $status) + (change-state! &cfv2-current-frame-mode $mode) + (change-state! &cfv2-current-hypotheses $hypotheses) + (change-state! &cfv2-current-method $methods) + (change-state! &cfv2-current-history-summary $historySummary) + (change-state! &cfv2-current-modules $modules) + (change-state! &cfv2-current-budget $budget) + (change-state! &cfv2-current-constraints $constraint) + (change-state! &cfv2-current-deliverables $deliverables) + (change-state! &cfv2-current-sub-frame-namespace $subFrameSpaceID) + (change-state! &cfv2-current-results $result) + (change-state! &cfv2-current-created-at $createdAt) + (change-state! &cfv2-current-updated-at $updatedAt) + (change-state! &cfv2-current-completed-timestamp $completedTimestamp) + (change-state! &cfv2-current-completed-summary $completedSummary) + + ((CurrentFrame (cfv2-current-frame)) + (Relation (collapse $relation)))) ;; return the frame state after loading and its relation + "FRAME MODE NOT ALLOWED FOR ROOT MODE, SWITCH TO A FRAME HAVING THE SAME MODE AND AS THE ROOT FRAME OR CONSIDER CHANGING THE ROOT MODE" + ))) + +;; Build a Frame atom from the current-frame cache states. +(= (cfv2-current-cache-to-frame) + (Frame + (frameID (cfv2-root-current-frame-id)) + (parent-frameID (get-state &cfv2-current-parent-frame-id)) + (source (get-state &cfv2-current-source)) + (priority (get-state &cfv2-current-priority)) + (goal-namespace (get-state &cfv2-current-goal-namespace)) + (status (get-state &cfv2-current-status)) + (frame-mode (get-state &cfv2-current-frame-mode)) + (hypotheses (get-state &cfv2-current-hypotheses)) + (method (get-state &cfv2-current-method)) + (history-summary (get-state &cfv2-current-history-summary)) + (modules (get-state &cfv2-current-modules)) + (budget (get-state &cfv2-current-budget)) + (constraints (get-state &cfv2-current-constraints)) + (deliverables (get-state &cfv2-current-deliverables)) + (sub-frame-namespace (get-state &cfv2-current-sub-frame-namespace)) + (results (get-state &cfv2-current-results)) + (created-at (get-state &cfv2-current-created-at)) + (updated-at (get-state &cfv2-current-updated-at)) + (completed-timestamp (get-state &cfv2-current-completed-timestamp)) + (completed-summary (get-state &cfv2-current-completed-summary)))) + + +;; TODO: make this generic for all use-case not only for frame ref +;; Update: The function is now generic for all use-cases, not only for frame ref. +;; Also it now replaces the match implementation with state operation. +(= (cfv2-check-frame-id (Frame (frameID $currentID) $parentID $source $priority $goalNamespace $status $frameMode $hypotheses $method $historySummary + $modules $budget $constraints $deliverables $subFrameNamespace $results $createdAt $updatedAt $completedTimestamp $completedSummary) + $frameID) + (if (== $currentID $frameID) True False)) + +(= (cfv2-check-frame-id (FrameRef (frameID $currentID) $parentFrameID $space + $source $priority (status $refStatus) $frameMode + $summary $createdAt $updatedAt $completedTimestamp) + $frameID) + (if (== $currentID $frameID) True False)) + +;; A more generic approach as an alternative for cfv2-frame-by-id and cfv2_latest_frame_by_id +(= (cfv2-get-frame $frameID $space) + (car-atom (filter-atom (get-state $space) $frame + (== (cfv2-check-frame-id $frame $frameID) True)))) + +;; Removes a frame or frame ref from a space by the given ID. +;; Works for both Frame and FrameRef atoms via cfv2-check-frame-id pattern dispatch. +(= (cfv2-remove-frame $frameID $frameSpace) + (filter-atom (get-state $frameSpace) $frame + (not (== (cfv2-check-frame-id $frame $frameID) True)))) + +(= (cfv2-select-next-frame) + (let $nextID + (py-call + (helper.cfv2_select_next_frame_id + (repr (cfv2-frame-refs-by-status Active)) + (repr (get-state &cfv2-root-mode)))) + (if (== $nextID NON) + (progn + (cfv2-clear-current-frame-cache) + (NO-ACTIVE-FRAME-FOR-ROOT-MODE)) + (progn + ; (log DEBUG "context" (SELECTING-NEXT-FRAME $nextID)) + (cfv2-load-frame $nextID)) + ))) + +(= (cfv2-check-ref-status (FrameRef $frameID $parentFrameID $space + $source $priority (status $currentStatus) $frameMode + $summary $createdAt $updatedAt $completedTimestamp) + $status) + (if (== $currentStatus $status) True False) +) + +(= (cfv2-frame-refs-by-status $status) + (if (== (cfv2FrameIndexSpace) ()) (FRAME-REF-EMPTY) + (filter-atom (cfv2FrameIndexSpace) $frameRef + (== (cfv2-check-ref-status $frameRef $status) True))) +) + +(= (cfv2-completed-frame-refs-after $datePrefix) + (sread + (py-call + (helper.cfv2_refs_completed_after + (repr (get-state &cfv2-frame-indexspace)) + (repr $datePrefix))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Completion / STM / LTM +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(= (cfv2-completed-frame-memory $storage $summary) + (py-str + ("HyperClawFrameMemory " + "kind=CompletedFrame " + "storage=" $storage " " + "time=" (cfv2-now) " " + "frameID=" (get-state &cfv2-current-frame-id) " " + "source=" (get-state &cfv2-current-source) " " + "mode=" (get-state &cfv2-current-frame-mode) " " + "priority=" (get-state &cfv2-current-priority) " " + "deliverables=" (cfv2-compact-plain (get-state &cfv2-current-deliverables)) " " + "method=" (cfv2-compact-plain (get-state &cfv2-current-method)) " " + "results=" (cfv2-compact-plain (get-state &cfv2-current-results)) " " + "history=" (cfv2-compact-plain (get-state &cfv2-current-history-summary)) " " + "summary=" $summary))) + +(= (cfv2-mark-current-frame-completed $summary) + (progn + (change-state! &cfv2-current-status Completed) + (change-state! &cfv2-current-completed-timestamp (cfv2-now)) + (change-state! &cfv2-current-completed-summary $summary) + (cfv2-update-current-history-summary FrameCompleted $summary) + (cfv2-touch-current-frame) + ; (cfv2-current-frame) + )) + +(= (cfv2-complete-current-frame-to-stm $summary) + (if (== (get-state &cfv2-current-frame-id) ()) + (progn + ; (log WARNING "context" ("No current frame to complete; root current frame: " (cfv2-root-current-frame-id))) + (NO-CURRENT-FRAME-TO-COMPLETE)) + (let $memory (cfv2-completed-frame-memory STM $summary) + (progn + (pin $memory) + (cfv2-mark-current-frame-completed $summary) + (change-state! &cfv2-active-framespace (cfv2-remove-frame (cfv2-root-current-frame-id) &cfv2-active-framespace)) + (change-state! &cfv2-frame-indexspace (cfv2-remove-frame (cfv2-root-current-frame-id) &cfv2-frame-indexspace)) + (cfv2-snapshot-current-frame Completed (cfv2-current-cache-to-frame)) + (cfv2-index-current-frame () Completed) + (cfv2-clear-current-frame-cache) + (cfv2-select-next-frame) + FRAME-COMPLETED-STORED-STM)))) + +(= (cfv2-complete-current-frame-to-ltm $summary) + (if (== (get-state &cfv2-current-frame-id) ()) + NO-CURRENT-FRAME-TO-COMPLETE + (let $memory (cfv2-completed-frame-memory LTM $summary) + (progn + (remember $memory) + (cfv2-mark-current-frame-completed $summary) + (cfv2-snapshot-current-frame Completed (cfv2-current-cache-to-frame)) + (cfv2-index-current-frame () Completed) + (change-state! &cfv2-active-framespace (cfv2-remove-frame (cfv2-root-current-frame-id) &cfv2-active-framespace)) + (change-state! &cfv2-frame-indexspace (cfv2-remove-frame (cfv2-root-current-frame-id) &cfv2-frame-indexspace)) + (cfv2-clear-current-frame-cache) + (cfv2-select-next-frame) + FRAME-COMPLETED-STORED-LTM)))) + +;; skipped cfv2-comapact-current-frame and cfv2-clear-current-frame-junk +;; Reason: the first is not needed, not for now, the second is redundant +;;TODO: evaluate thorologly of clear-frame-junk, is it really needed if we have a clear cache function that is called when there is no current frame or after completion and switching to the next frame? +(= (cfv2-clear-current-frame-junk $summary) + (if (== (get-state &cfv2-current-frame-id) ()) + NO-CURRENT-FRAME-TO-CLEAR + (progn + (change-state! &cfv2-current-hypotheses ()) + (change-state! &cfv2-current-results ()) + (cfv2-update-current-history-summary FrameJunkCleared $summary) + (cfv2-touch-current-frame) + ; (cfv2-snapshot-current-frame Active (cfv2-current-cache-to-frame)) + ; (cfv2-index-current-frame Active) + FRAME-JUNK-CLEARED))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Maintenance +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(= (cfv2-has-current-frame) + (not (== (get-state &cfv2-current-frame-id) ()))) + +;; TODO: implement compact frame retrival +(= (cfv2-maintain-frame) + (if (cfv2-has-current-frame) + (progn + (cfv2-touch-current-frame) + (cfv2-current-frame)) + (cfv2-select-next-frame))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; User-facing / skill-friendly helpers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(= (new-frame $description) + (cfv2-create-frame-from-user-message $description)) + +(= (new-autonomous-frame $description) + (cfv2-create-autonomous-frame $description 0.5 Slow)) + +(= (switch-frame $frameID) + (cfv2-load-frame $frameID)) + +(= (show-root-frame) + (cfv2-root-frame)) + +(= (show-current-frame) + (cfv2-current-frame)) + +(= (show-frame-index) + (get-state &cfv2-frame-indexspace)) + +(= (show-active-framespace) + (get-state &cfv2-active-framespace)) + +(= (show-completed-framespace) + (get-state &cfv2-completed-framespace)) + +(= (show-frame-relation $frameID) + (cfv2-get-relation $frameID)) + +(= (switch-mode) + (cfv2-switch-mode)) + +(= (send_probe) (progn (change-state! &loops 25) ("ALIVE PROBE SENT"))) + +; (= (create-sub-frame $description) +; (cfv2-create-sub-frame $description 0.8 (cfv2-make-deliverable $description))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Backward compatibility with previous context.metta / loop.metta +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(= (initContextFrame) + (cfv2-init-context-frames)) + +(= (currentRootFrame) + (cfv2-root-frame)) + +(= (currentContextFrame) + (cfv2-current-frame)) + +(= (contextFrameForPrompt) + (swrite (cfv2-context-projection))) + +;; TODO: this implementation needs to change see comment on cfv2-ingest-user-message +(= (ctx-ingest-user-message $msg) + (cfv2-ingest-user-message $msg)) + +(= (ctx-record-command-batch $commands $results) + (cfv2-record-command-batch $commands $results)) + +(= (ctx-maintain-frame) + (cfv2-maintain-frame)) + +(= (ctx-has-active-goals) + (cfv2-has-current-frame)) + +(= (ctx-compact-plain $value) + (cfv2-compact-plain $value)) + +(= (ctx-add-hypothesis $id $hypothesis) + (cfv2-add-hypothesis $id $hypothesis)) + +(= (ctx-add-result $variant $metrics) + (cfv2-add-result $variant $metrics)) + +(= (ctx-set-certified-method $description $parameters $evalProtocol $hash) + (cfv2-set-certified-method $description $parameters $evalProtocol $hash)) + +(= (ctx-add-deliverable $artifact) + (cfv2-add-deliverable $artifact)) + +(= (complete-goals-stm $summary) + (once (cfv2-complete-current-frame-to-stm $summary))) + +(= (complete-goals-ltm $summary) + (once (cfv2-complete-current-frame-to-ltm $summary))) + +(= (clear-frame-junk $summary) + (once (cfv2-clear-current-frame-junk $summary))) diff --git a/src/frame_relation.py b/src/frame_relation.py new file mode 100644 index 00000000..59e03af3 --- /dev/null +++ b/src/frame_relation.py @@ -0,0 +1,596 @@ +from __future__ import annotations + +import hashlib +import json +import os +import re +from typing import Any + +import chromadb +from openai import OpenAI +import lib_llm_ext +from config import config_get_by_key + +CHROMA_DB_PATH = os.environ.get("CHROMA_DB_PATH", "./chroma_db") +FRAME_SKETCH_COLLECTION_BASE = os.environ.get("FRAME_SKETCH_COLLECTION", "cfv2_frame_sketches") +FRAME_EMBED_MODEL = os.environ.get("FRAME_EMBED_MODEL", "text-embedding-3-large") + +_chroma_client = None +_collections: dict[str, Any] = {} +_openai_client = None +_local_embedding_ready = False + + +def _provider_name(provider: Any) -> str: + p = str(provider or "OpenAI").strip().strip('"') + p = re.sub(r"[^A-Za-z0-9_\-]", "", p) + return p or "OpenAI" + + +def _collection_name(provider: str) -> str: + # Separate collections prevent dimension conflicts between OpenAI and Local embeddings. + return f"{FRAME_SKETCH_COLLECTION_BASE}_{provider.lower()}"[:63] + + +def _get_collection(provider: str): + global _chroma_client, _collections + provider = _provider_name(provider) + name = _collection_name(provider) + if name not in _collections: + if _chroma_client is None: + _chroma_client = chromadb.PersistentClient(path=CHROMA_DB_PATH) + _collections[name] = _chroma_client.get_or_create_collection( + name=name, + embedding_function=None, + ) + return _collections[name] + + +def _get_openai_client(): + global _openai_client + if _openai_client is None: + _openai_client = OpenAI() + return _openai_client + + +# ----------------------------------------------------------------------------- +# S-expression helpers +# ----------------------------------------------------------------------------- + +def _balanced_end(s: str, start: int) -> int: + depth = 0 + in_string = False + escaped = False + for i in range(start, len(s)): + ch = s[i] + if in_string: + if escaped: + escaped = False + elif ch == "\\": + escaped = True + elif ch == '"': + in_string = False + continue + if ch == '"': + in_string = True + elif ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0: + return i + 1 + return len(s) + + +def _find_exprs_with_head(s: str, head: str) -> list[str]: + out: list[str] = [] + needle = f"({head}" + i = 0 + while True: + i = s.find(needle, i) + if i < 0: + break + after = i + len(needle) + if after < len(s) and not s[after].isspace() and s[after] != ")": + i = after + continue + end = _balanced_end(s, i) + out.append(s[i:end]) + i = end + return out + + +def _field(expr: str, name: str, default: str = "") -> str: + needle = f"({name}" + i = 0 + while True: + i = expr.find(needle, i) + if i < 0: + return default + after = i + len(needle) + if after < len(expr) and not expr[after].isspace() and expr[after] != ")": + i = after + continue + end = _balanced_end(expr, i) + inner = expr[i + 1:end - 1].strip() + if inner == name: + return default + return inner[len(name):].strip() + + +def _first_field(expr: str, names: list[str], default: str = "") -> str: + for name in names: + value = _field(expr, name, "") + if value not in ("", "()"): + return value + return default + + +def _strip_outer_quotes(x: Any) -> str: + s = "" if x is None else str(x).strip() + if len(s) >= 2 and s[0] == '"' and s[-1] == '"': + return s[1:-1] + return s + + +def _compact(x: Any, limit: int = 900) -> str: + s = _strip_outer_quotes(x) + s = re.sub(r"\s+", " ", s).strip() + return s if len(s) <= limit else s[:limit - 16] + "..." + + +def _sym(x: Any, default: str = "UNKNOWN") -> str: + s = _strip_outer_quotes(x) + s = re.sub(r"[^A-Za-z0-9_\-:.]", "", s) + return s or default + + +def _quote(x: Any) -> str: + s = "" if x is None else str(x) + s = re.sub(r"\s+", " ", s).strip() + s = s.replace("\\", "\\\\").replace('"', '\\"') + return f'"{s}"' + + +def _float(x: Any, default: float = 0.0) -> float: + try: + return float(_strip_outer_quotes(x)) + except Exception: + return default + + +def _hash_text(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + +# ----------------------------------------------------------------------------- +# Frame parsing/document creation +# ----------------------------------------------------------------------------- + +def _parse_frame_sketches(compact_frames_repr: str) -> list[dict[str, Any]]: + """ + Accepts compact Frame atoms, not FrameSketch atoms. + + Expected: + ((Frame (frameID FrameA) (parentID ParentA) (status Active) + (priority 1.0) (deliverable "goal") (results "summary")) ...) + """ + frames: list[dict[str, Any]] = [] + for expr in _find_exprs_with_head(str(compact_frames_repr), "Frame"): + frame_id = _first_field(expr, ["frameID", "FrameID"], "") + if frame_id in ("", "()"): + continue + frames.append({ + "frameID": _sym(frame_id), + "parentID": _sym(_first_field(expr, ["parentID", "parent-frameID"], "")), + "status": _sym(_first_field(expr, ["status"], "")), + "priority": _float(_first_field(expr, ["priority"], "0.0")), + "deliverable": _compact(_first_field(expr, ["deliverable", "deliverables"], ""), 900), + "results": _compact(_first_field(expr, ["results"], ""), 900), + "source": _sym(_first_field(expr, ["source"], "")), + "mode": _sym(_first_field(expr, ["mode", "frame-mode"], "")), + }) + return frames + + +def _frame_document(frame: dict[str, Any]) -> str: + # This exact text is embedded and stored. + return ( + f"(Frame " + f"(frameID {frame['frameID']}) " + f"(parentID {frame['parentID']}) " + f"(status {frame['status']}) " + f"(priority {frame['priority']}) " + f"(deliverable {frame['deliverable']}) " + f"(results {frame['results']}))" + ) + + +def _frame_metadata(frame: dict[str, Any], provider: str, content_hash: str) -> dict[str, Any]: + return { + "frameID": frame["frameID"], + "parentID": frame["parentID"], + "status": frame["status"], + "priority": float(frame["priority"]), + "source": frame["source"], + "mode": frame["mode"], + "embeddingProvider": provider, + "contentHash": content_hash, + } + + +# ----------------------------------------------------------------------------- +# Embedding providers +# ----------------------------------------------------------------------------- + +def _coerce_vector(value: Any) -> list[float]: + if value is None: + return [] + if isinstance(value, (list, tuple)): + return [float(x) for x in value] + if hasattr(value, "tolist"): + return [float(x) for x in value.tolist()] + text = str(value).strip().replace("[", " ").replace("]", " ") + text = text.replace("(", " ").replace(")", " ").replace(",", " ") + nums = re.findall(r"[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?", text) + return [float(n) for n in nums] + + +def _embed_texts_openai(texts: list[str]) -> list[list[float]]: + if not texts: + return [] + client = _get_openai_client() + response = client.embeddings.create(model=FRAME_EMBED_MODEL, input=texts) + return [list(item.embedding) for item in response.data] + + +def _embed_texts_local(texts: list[str]) -> list[list[float]]: + """ + Uses your existing local embedding module: + lib_llm_ext.initLocalEmbedding() + lib_llm_ext.useLocalEmbedding(text) + """ + global _local_embedding_ready + if not texts: + return [] + + if not _local_embedding_ready: + try: + lib_llm_ext.initLocalEmbedding() + except Exception: + pass + _local_embedding_ready = True + + return [_coerce_vector(lib_llm_ext.useLocalEmbedding(str(t))) for t in texts] + + +def _embed_texts(texts: list[str], provider: str) -> list[list[float]]: + provider = _provider_name(provider) + if provider.lower() == "openai": + return _embed_texts_openai(texts) + if provider.lower() == "local": + return _embed_texts_local(texts) + raise ValueError(f"Unknown embedding provider: {provider}") + + +# ----------------------------------------------------------------------------- +# Chroma upsert/search using explicit embeddings +# ----------------------------------------------------------------------------- + +def _existing_hashes(collection, ids: list[str]) -> dict[str, str]: + if not ids: + return {} + try: + result = collection.get(ids=ids, include=["metadatas"]) + except Exception: + return {} + out: dict[str, str] = {} + for fid, meta in zip(result.get("ids", []) or [], result.get("metadatas", []) or []): + if meta and "contentHash" in meta: + out[str(fid)] = str(meta["contentHash"]) + return out + + +def _upsert_changed_frames(frames: list[dict[str, Any]], provider: str) -> dict[str, list[float]]: + """ + Upserts only new/changed frames. + Returns embeddings computed during this call: frameID -> embedding. + """ + if not frames: + return {} + + provider = _provider_name(provider) + collection = _get_collection(provider) + + ids = [f["frameID"] for f in frames if f["frameID"] != "UNKNOWN"] + old_hashes = _existing_hashes(collection, ids) + + changed_frames = [] + changed_docs = [] + changed_hashes = [] + + for frame in frames: + fid = frame["frameID"] + if not fid or fid == "UNKNOWN": + continue + doc = _frame_document(frame) + content_hash = _hash_text(f"{provider}:{FRAME_EMBED_MODEL}:{doc}") + if old_hashes.get(fid) == content_hash: + continue + changed_frames.append(frame) + changed_docs.append(doc) + changed_hashes.append(content_hash) + + if not changed_frames: + return {} + + embeddings = _embed_texts(changed_docs, provider) + computed: dict[str, list[float]] = {} + + upsert_ids = [] + upsert_docs = [] + upsert_metas = [] + upsert_embeddings = [] + + for frame, doc, content_hash, emb in zip(changed_frames, changed_docs, changed_hashes, embeddings): + fid = frame["frameID"] + computed[fid] = emb + upsert_ids.append(fid) + upsert_docs.append(doc) + upsert_metas.append(_frame_metadata(frame, provider, content_hash)) + upsert_embeddings.append(emb) + + collection.upsert( + ids=upsert_ids, + embeddings=upsert_embeddings, + documents=upsert_docs, + metadatas=upsert_metas, + ) + + return computed + + +def _search_top_k(query_frame: dict[str, Any], query_embedding: list[float], provider: str, top_k: int) -> list[dict[str, Any]]: + if not query_embedding: + return [] + + collection = _get_collection(provider) + if collection.count() <= 1: + return [] + + result = collection.query( + query_embeddings=[query_embedding], + n_results=max(1, int(top_k) + 1), + include=["documents", "metadatas", "distances"], + ) + + ids = result.get("ids", [[]])[0] + docs = result.get("documents", [[]])[0] + metas = result.get("metadatas", [[]])[0] + distances = result.get("distances", [[]])[0] + + hits: list[dict[str, Any]] = [] + for hit_id, doc, meta, distance in zip(ids, docs, metas, distances): + if hit_id == query_frame["frameID"]: + continue + hits.append({ + "frameID": str(hit_id), + "document": doc, + "metadata": meta or {}, + "distance": float(distance), + }) + if len(hits) >= int(top_k): + break + return hits + + +# ----------------------------------------------------------------------------- +# Classifier +# ----------------------------------------------------------------------------- + +def _parse_relation_classes(relation_classes_repr: str) -> list[str]: + classes = re.findall(r"[A-Za-z][A-Za-z0-9_\-]*", str(relation_classes_repr)) + ignored = {"RelationClasses", "ClassList", "List", "Set", "Class", "Classes"} + classes = [c for c in classes if c not in ignored] + return list(dict.fromkeys(classes)) if classes else ["RelatedButSeparate", "Unrelated"] + + +def _call_selected_llm(content: str, max_tokens: int, reasoning_mode: str) -> str: + import providers + + chat = getattr(providers, "llmProviderChat", None) + if chat is None: + raise RuntimeError("OmegaClaw LLM provider registry is not loaded") + return str(chat(content, max_tokens, reasoning_mode) or "") + + +def _call_classifier_llm(payload: dict[str, Any]) -> dict[str, Any]: + system_prompt = """ +You classify the relationship between one query frame and each candidate frame. +The purpose is to compose these frames in order to create a more sound and coherent +context-frame. + +Return only valid JSON with this schema: +{ + "relations": [ + { + "frameID1": "query frame id", + "frameID2": "candidate frame id", + "class": "one allowed relation class", + "reason": "short reason", + "confidence": 0.0 + } + ] +} + +Rules: +- Use only the allowed relation classes. +- frameID1 must be the query frame ID. +- frameID2 must be one of the candidate frame IDs. +- Confidence must be between 0 and 1. +- Be conservative. +- If related but unsafe to merge/compose, use RelatedButSeparate if available. +- If unrelated, do not include them in your answer. +- Do not invent frame IDs. +- Do not output markdown. +""".strip() + + user_text = json.dumps(payload, ensure_ascii=False) + content = ( + f"{system_prompt}" + f"{lib_llm_ext.PROMPT_DELIMITER}" + f"{user_text}" + ) + max_tokens = max( + 1, + int(config_get_by_key("maxOutputToken", 6000)), + ) + reasoning_mode = str( + config_get_by_key("reasoningMode", "medium") + ) + raw = _call_selected_llm(content, max_tokens, reasoning_mode).strip() + + try: + return json.loads(raw) + except Exception: + match = re.search(r"\{.*\}", raw, flags=re.S) + return json.loads(match.group(0)) if match else {"relations": []} + + +def _classify_relations(query_frame: dict[str, Any], hits: list[dict[str, Any]], relation_classes: list[str]) -> list[dict[str, Any]]: + if not hits: + return [] + + payload = { + "allowed_relation_classes": relation_classes, + "query_frame": { + "frameID": query_frame["frameID"], + "parentID": query_frame["parentID"], + "status": query_frame["status"], + "priority": query_frame["priority"], + "deliverable": query_frame["deliverable"], + "results": query_frame["results"], + }, + "candidate_frames": [ + { + "frameID": hit["frameID"], + "distance": hit["distance"], + "document": hit["document"], + "metadata": hit["metadata"], + } + for hit in hits + ], + } + + data = _call_classifier_llm(payload) + allowed = set(relation_classes) + candidate_ids = {hit["frameID"] for hit in hits} + + if "Unrelated" in allowed: + default_class = "Unrelated" + elif "RelatedButSeparate" in allowed: + default_class = "RelatedButSeparate" + else: + default_class = relation_classes[0] + + clean: list[dict[str, Any]] = [] + for item in data.get("relations", []): + frame_id_1 = _sym(item.get("frameID1", query_frame["frameID"])) + frame_id_2 = _sym(item.get("frameID2", "")) + + if frame_id_1 != query_frame["frameID"]: + frame_id_1 = query_frame["frameID"] + if frame_id_2 not in candidate_ids: + continue + + rel_class = _sym(item.get("class", default_class)) + if rel_class not in allowed: + rel_class = default_class + + confidence = max(0.0, min(1.0, _float(item.get("confidence", 0.0), 0.0))) + clean.append({ + "frameID1": frame_id_1, + "frameID2": frame_id_2, + "class": rel_class, + "reason": _compact(item.get("reason", ""), 300), + "confidence": confidence, + }) + return clean + + +def _relations_to_sexpr(relations: list[dict[str, Any]]) -> str: + if not relations: + return "()" + atoms = [] + for relation in relations: + atoms.append( + f"(Relation " + f"(FrameID-1 {relation['frameID1']}) " + f"(FrameID-2 {relation['frameID2']}) " + f"(Class {relation['class']}) " + f"(Reason {_quote(relation['reason'])}) " + f"(Confidence {relation['confidence']:.4f}))" + ) + return f"({' '.join(atoms)})" + + +# ----------------------------------------------------------------------------- +# Main MeTTa py-call entrypoint +# ----------------------------------------------------------------------------- + +def cfv2_compose_frame_relations( + compact_frames_repr: str, + query_frame_id_repr: str, + relation_classes_repr: str, + embedding_provider_repr: str = "OpenAI", + top_k: int = 5, +) -> str: + """ + Args: + compact_frames_repr: + repr string containing compact Frame atoms, with no embedding field. + + query_frame_id_repr: + current/new frame ID. + + relation_classes_repr: + allowed classes, e.g. + (DuplicateOf ContinuationOf SubgoalOf ParentOf DependsOn Blocks + Supersedes SameProject SameFailureCluster RelatedButSeparate Unrelated) + + embedding_provider_repr: + OpenAI or Local. Local calls lib_llm_ext.useLocalEmbedding. + + top_k: + retrieved candidate count. + + Returns: + String S-expression: + ((Relation (FrameID-1 FrameA) (FrameID-2 FrameB) + (Class ContinuationOf) (Reason "...") (Confidence 0.8600)) ...) + """ + provider = _provider_name(embedding_provider_repr) + frames = _parse_frame_sketches(compact_frames_repr) + query_frame_id = _sym(query_frame_id_repr) + relation_classes = _parse_relation_classes(relation_classes_repr) + + if not frames or not query_frame_id: + return "()" + + frame_by_id = {frame["frameID"]: frame for frame in frames} + query_frame = frame_by_id.get(query_frame_id) + if query_frame is None: + return "()" + + computed_embeddings = _upsert_changed_frames(frames, provider) + + query_embedding = computed_embeddings.get(query_frame_id) + if query_embedding is None: + query_embedding = _embed_texts([_frame_document(query_frame)], provider)[0] + + hits = _search_top_k(query_frame, query_embedding, provider, top_k) + if not hits: + return "()" + + relations = _classify_relations(query_frame, hits, relation_classes) + return _relations_to_sexpr(relations) diff --git a/src/helper.py b/src/helper.py index cf2a2765..c43e46c9 100644 --- a/src/helper.py +++ b/src/helper.py @@ -1,7 +1,9 @@ from collections import deque import json import re +import hashlib from datetime import datetime +from typing import Dict, List, Optional, Tuple import os try: @@ -14,15 +16,32 @@ TS_RE = re.compile(r'^\("(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"') LLM_COMMANDS = { "append-file", + "clear-frame-junk", + "compact-frame", + "complete-goals-ltm", + "complete-goals-stm", + "ctx-add-hypothesis", + "ctx-add-result", "episodes", "metta", + "new-autonomous-frame", + "new-frame", "pin", "query", "read-file", "remember", - "search", + "websearch", "send", + "send_probe", "shell", + "show-active-framespace", + "show-completed-framespace", + "show-current-frame", + "show-frame-index", + "show-frame-relation", + "show-root-frame", + "switch-frame", + "switch-mode", "tavily-search", "technical-analysis", "write-file", @@ -32,9 +51,32 @@ TWO_ARG_COMMANDS = { "write-file", "append-file", - "write-file-b64" + "write-file-b64", + "ctx-add-hypothesis", + "ctx-add-result", } +def compact_plain(value, limit=1200): + """ + Return a compact, single-line summary with a stable digest. + This does not write files and does not store to LTM. + MeTTa decides whether to pin/remember the resulting summary. + """ + text = normalize_string(value) + compact = re.sub(r"\s+", " ", text).strip() + digest = hashlib.sha256(text.encode("utf-8", errors="ignore")).hexdigest() + + if len(compact) > int(limit): + compact = compact[: int(limit) - 3].rstrip() + "..." + + return f"sha256:{digest[:16]} chars:{len(text)} excerpt:{compact}" + + +def make_id(prefix="id"): + stamp = datetime.utcnow().strftime("%Y%m%dT%H%M%S%fZ") + return f"{prefix}-{stamp}" + + def extract_timestamp(line): m = TS_RE.search(line) if not m: @@ -93,6 +135,47 @@ def starts_command_line(line): first = s.split(maxsplit=1)[0].rstrip(")") return first in LLM_COMMANDS +def split_toplevel_forms(line): + """Split a line holding several complete s-expressions into separate forms. + + Parentheses inside string literals are ignored. A line that is not a plain + sequence of balanced top-level forms is returned unchanged, so single-form + answers and free text keep their previous handling. + """ + forms = [] + depth = 0 + start = None + in_string = False + escaped = False + for i, ch in enumerate(line): + if in_string: + if escaped: + escaped = False + elif ch == "\\": + escaped = True + elif ch == '"': + in_string = False + continue + if ch == '"': + in_string = True + elif ch == "(": + if depth == 0: + start = i + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0 and start is not None: + forms.append(line[start:i + 1]) + start = None + elif depth < 0: + return [line] + elif depth == 0 and not ch.isspace(): + return [line] + if depth != 0 or len(forms) < 2: + return [line] + return forms + + def split_command_blocks(s): blocks = [] cur = [] @@ -108,7 +191,10 @@ def split_command_blocks(s): cur.append(raw) if cur: blocks.append("\n".join(cur).strip()) - return blocks + expanded = [] + for block in blocks: + expanded.extend(split_toplevel_forms(block.strip())) + return expanded def balance_parentheses(s): s = s.replace("_quote_", '"').replace("_newline_", "\n") @@ -184,6 +270,162 @@ def joinPath(parts): def projectRootDirectory(): return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# ---- HyperClaw Context Frames V2 helper additions ---- + +def cfv2_now() -> str: + return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") + + +def _unescape_repr_id(value: str) -> str: + value = str(value).strip() + value = value.replace("'", "").replace('"', "") + value = value.replace("[", "").replace("]", "") + return value.strip() + + +def _balanced_exprs(text: str, head: str) -> List[str]: + """Extract top-level balanced s-expressions whose head is `head`. + + This is a pragmatic parser for scorer/runtime helper use. It is not a full MeTTa parser, + but it handles strings and nested parentheses well enough for Frame/FrameRef atoms. + """ + text = str(text) + starts = [] + token = f"({head}" + i = 0 + while True: + idx = text.find(token, i) + if idx < 0: + break + starts.append(idx) + i = idx + len(token) + + out = [] + for start in starts: + depth = 0 + in_str = False + escaped = False + for j in range(start, len(text)): + ch = text[j] + if in_str: + if escaped: + escaped = False + elif ch == "\\": + escaped = True + elif ch == '"': + in_str = False + continue + if ch == '"': + in_str = True + elif ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0: + out.append(text[start : j + 1]) + break + return out + + +def _field(expr: str, field_name: str) -> Optional[str]: + """Return the raw value of a first-level-ish `(field value)` form. + + This intentionally works on the stable constructor format emitted by the MeTTa code. + """ + pattern = f"({field_name}" + idx = expr.find(pattern) + if idx < 0: + return None + start = idx + len(pattern) + # Skip whitespace. + while start < len(expr) and expr[start].isspace(): + start += 1 + if start >= len(expr): + return None + if expr[start] == "(": + depth = 0 + in_str = False + escaped = False + for j in range(start, len(expr)): + ch = expr[j] + if in_str: + if escaped: + escaped = False + elif ch == "\\": + escaped = True + elif ch == '"': + in_str = False + continue + if ch == '"': + in_str = True + elif ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0: + return expr[start : j + 1] + return None + if expr[start] == '"': + escaped = False + for j in range(start + 1, len(expr)): + ch = expr[j] + if escaped: + escaped = False + elif ch == "\\": + escaped = True + elif ch == '"': + return expr[start : j + 1] + return None + # Atom/number until whitespace or close paren. + end = start + while end < len(expr) and not expr[end].isspace() and expr[end] != ")": + end += 1 + return expr[start:end] + +def cfv2_refs_completed_after(index_repr, date_prefix) -> str: + """Return completed FrameRefs whose completed-timestamp starts with or compares after date_prefix. + + date_prefix can be YYYY-MM-DD or a longer timestamp prefix. This is intentionally simple. + """ + prefix = _unescape_repr_id(date_prefix) + refs = [] + for ref in _balanced_exprs(str(index_repr), "FrameRef"): + status = _unescape_repr_id(_field(ref, "status") or "") + t = _unescape_repr_id(_field(ref, "completed-timestamp") or "") + if status == "Completed" and t and t >= prefix: + refs.append(ref) + return "(" + " ".join(refs) + ")" + + +def cfv2_select_next_frame_id(index_repr, root_mode="Fast") -> str: + """Select highest-priority active frame matching root mode from FrameRef space. + + If multiple FrameRefs exist for a frame, the last one wins. This supports append-only refs. + """ + mode = _unescape_repr_id(root_mode) + latest: Dict[str, Tuple[float, str, str, str]] = {} + for ref in _balanced_exprs(str(index_repr), "FrameRef"): + fid = _unescape_repr_id(_field(ref, "frameID") or "") + status = _unescape_repr_id(_field(ref, "status") or "") + frame_mode = _unescape_repr_id(_field(ref, "frame-mode") or "") + space = _unescape_repr_id(_field(ref, "space") or "") + priority_raw = _unescape_repr_id(_field(ref, "priority") or "0") + try: + priority = float(priority_raw) + except Exception: + priority = 0.0 + if fid: + latest[fid] = (priority, status, frame_mode, space) + + best_id = "NON" + best_priority = float("-inf") + for fid, (priority, status, frame_mode, space) in latest.items(): + if space == "Active" and status in {"Active", "Focused"} and frame_mode == mode: + if priority > best_priority: + best_priority = priority + best_id = fid + return best_id + def test_balance_parenthesis(): assert balance_parentheses('(write-file test.txt hello world)') == '((write-file "test.txt" "hello world"))' assert balance_parentheses('(append-file test.txt hello world)') == '((append-file "test.txt" "hello world"))' diff --git a/src/loop.metta b/src/loop.metta index 8d07507f..15122ad8 100644 --- a/src/loop.metta +++ b/src/loop.metta @@ -6,12 +6,10 @@ (= (reasoningMode) (empty)) (= (wakeupInterval) (empty)) (= (memoryDirectory) (empty)) -(= (spamShield) (empty)) ; TODO: this parameter is considered deprecated (= (initLoop) - (progn (configure maxNewInputLoops 50) ;20 + (progn (configure maxNewInputLoops 50) (configure maxWakeLoops 1) - (configure spamShield False) (configure sleepInterval 1) ;10 (configure provider Anthropic) (configure maxOutputToken 6000) @@ -29,24 +27,28 @@ (log INFO "loop" (py-call (rag.init_knowledge "OpenAI"))) (log INFO "loop" (py-call (rag.init_knowledge "Local")))))) -(= (getContext) - (string-safe (py-str ("PROMPT: " (getPrompt (provider)) " SKILLS: " (getSkills) - (newline) (getPromptExtensions) (newline) - " OUTPUT_FORMAT: Up to 5 lines, do not wrap quotes around args, do not use variables:" (newline) - " toolName1 arg1" (newline) - " toolName2 arg2" (newline) - " toolName3 arg3" (newline) - " toolName4 arg4" (newline) - " toolName5 arg5" (newline) - " SAVE_PERMANENT_FILES_DIR: " (memoryDirectory) - " LAST_SKILL_USE_RESULTS: you must rectify ALERT_FAILED skill use cases " (last_chars (get-state &lastresults) (maxFeedback)) - " HISTORY: " (getHistory) - " TIME: " (get_time_as_string))))) +(= (getContext) + (string-safe (py-str ("PROMPT: " (getPrompt (provider)) (newline) + "RUNTIME-PROMPT: " (getContextFramePrompt) (newline) + "CURRENT_CONTEXT_FRAME_S_EXPR: " (contextFrameForPrompt) (newline) + "SKILLS: " (getSkills) (newline) + "CONTEXT_FRAME_SKILLS: " (contextFramesSkills) (newline) + (getPromptExtensions) (newline) + "OUTPUT_FORMAT: Up to 5 lines, do not wrap quotes around args, do not use variables:" (newline) + "toolName1 arg1" (newline) + "toolName2 arg2" (newline) + "toolName3 arg3" (newline) + "toolName4 arg4" (newline) + "toolName5 arg5" (newline) + "SAVE_PERMANENT_FILES_DIR: " (memoryDirectory) (newline) + "LAST_SKILL_USE_RESULTS: you must rectify ALERT_FAILED skill use cases " + (last_chars (get-state &lastresults) (maxFeedback)) (newline) + "TIME: " (get_time_as_string))))) (= (getPromptExtensions) (join (newline) (collapse (prompt-extension $_)))) -; dynamic prompt extension placeholder to eliminate error when no extension is added +; Dynamic prompt extension placeholder to eliminate errors when no extension is added. (= (prompt-extension placeholder) (empty)) (= (HandleError $msg $cmd $sexpr) @@ -54,6 +56,10 @@ (progn (change-state! &error $new) (ALERT_FAILED $a $b)))) ($else $sexpr)))) +(= (metta $str) + (let $code (sread $str) + (repr (swrite (eval $code))))) + (= (omegaclaw) (omegaclaw 1)) (= (omegaclaw $k) @@ -65,39 +71,58 @@ (initKnowledge) (initPlugins) (initChannels) + (initContextFrame) (llmProviderStart (provider))) - (change-state! &loops (- (get-state &loops) 1))) - (let $prompt (getContext) - (progn (log INFO "loop" (---------iteration $k)) - (heartbeat $k) - (let* (($msgrcv (string-safe (repr (receive)))) - ($msgnew (prog1 (and (> (string_length $msgrcv) 0) (!= $msgrcv (get-state &prevmsg))) - (if (> (string_length $msgrcv) 0) (change-state! &prevmsg $msgrcv) _))) - ($msg (get-state &prevmsg)) - ($_ (if (and (> $k 1) $msgnew) - (change-state! &loops (maxNewInputLoops)) _))) - (if (> (get-state &loops) 0) - (let* (($lastmessage (if $msgnew (HUMAN-MSG: $msg) (if (spamShield) " DO NOT RE-SEND OR SPAM!" ""))) - ($_ (change-state! &nextWakeAt (+ (get_time) (wakeupInterval)))) - ($_ (log INFO "loop" $lastmessage)) - ($send (py-str ($prompt :-:-:-: $lastmessage))) - ($_ (log INFO "loop" (CHARS_SENT: (string_length $send) $send))) - ($respi (llmProviderChat $send (maxOutputToken) (reasoningMode))) - ($resp (py-call (helper.balance_parentheses $respi))) - ($response (if (== "(" (first_char $resp)) $resp (progn (log INFO "loop" $resp) (repr (REMEMBER:OUTPUT_NOTHING_ELSE_THAN: ((skill arg) ...)))))) - ($sexpr (catch (sread $response))) - ($_ (change-state! &error ())) - ($mcerr (HandleError MULTI_COMMAND_FAILURE_NOTHING_WAS_DONE_PLEASE_CORRECT_PARENTHESES_AND_USE_QUOTES_AND_RETRY $response $sexpr)) - ($_ (log INFO "loop" (RESPONSE: $sexpr))) - ($results (if (== $mcerr $sexpr) - (RESULTS: (collapse (let $s (superpose $sexpr) (COMMAND_RETURN: ($s (HandleError SINGLE_COMMAND_ERROR_NOTHING_WAS_DONE_PLEASE_FIX_AND_RETRY $s (catch (let $R (eval $s) (py-call (helper.normalize_string $R)))))))))) - (RESULTS: $mcerr))) - ($_ (log INFO "loop" (RESPONSE: $results)))) - (progn (if (or $msgnew (not (== $sexpr ()))) (addToHistory $msg $response $sexpr $msgnew) _) - (change-state! &lastresults (string-safe (repr $results))))) - (if (> (get_time) (get-state &nextWakeAt)) - (change-state! &loops (+ 1 (maxWakeLoops))) _))) - (sleep (sleepInterval)) + (change-state! &loops (- (get-state &loops) 1))) + + (log INFO "loop" (---------iteration $k)) + + (heartbeat $k) + (let* (($msgrcv (string-safe (repr (receive)))) + ($msgnew (prog1 (and (> (string_length $msgrcv) 0) (!= $msgrcv (get-state &prevmsg))) + (if (> (string_length $msgrcv) 0) (change-state! &prevmsg $msgrcv) _))) + ($msg (get-state &prevmsg)) + ($hadActiveFrame (if $msgnew (cfv2-has-current-frame) False)) + ; New input becomes frame state before prompting. + ($_ (if $msgnew (ctx-ingest-user-message $msg) _)) + ($_ (if (not (ctx-has-active-goals)) (ctx-maintain-frame) _)) + ($_ (if (and (> $k 1) $msgnew) (change-state! &loops (maxNewInputLoops)) _)) + ; Prompt is now frame-based. + ($prompt (getContext))) + + (if (> (get-state &loops) 0) + (let* (($loopSignal + (if $msgnew + (if $hadActiveFrame + "NEW_INPUT_HAS_BEEN_ADMITTED_AS_ACTIVE_FRAME. Continue current frame or switch-frame to a higher-priority admitted frame if appropriate." + "NEW_INPUT_HAS_BEEN_ADMITTED_AS_ACTIVE_FRAME.") + (if (cfv2-has-current-frame) + "NO_NEW_INPUT. Continue only if the context frame contains an active useful goal or select relevant frame from the active space." + "NO_NEW_INPUT. No active frame. Consider switching your mode to pursue frames registered with mode = Slow or create a new autonomous goal."))) + ($_ (change-state! &nextWakeAt (+ (get_time) (wakeupInterval)))) + ($_ (log INFO "loop" $loopSignal)) + ($send (py-str ($prompt :-:-:-: $loopSignal))) + ($_ (log INFO "loop" (CHARS_SENT: (string_length $send) $send))) + ($respi (llmProviderChat $send (maxOutputToken) (reasoningMode))) + ($resp (py-call (helper.balance_parentheses $respi))) + ($response (if (== "(" (first_char $resp)) $resp (progn (log INFO "loop" $resp) (repr (REMEMBER:OUTPUT_NOTHING_ELSE_THAN: ((skill arg) ...)))))) + ($sexpr (catch (sread $response))) + ($_ (change-state! &error ())) + ($mcerr (HandleError MULTI_COMMAND_FAILURE_NOTHING_WAS_DONE_PLEASE_CORRECT_PARENTHESES_AND_USE_QUOTES_AND_RETRY $response $sexpr)) + ($_ (log INFO "loop" (RESPONSE: $sexpr))) + ($results (if (== $mcerr $sexpr) + (RESULTS: (collapse (let $s (superpose $sexpr) (COMMAND_RETURN: ($s (HandleError SINGLE_COMMAND_ERROR_NOTHING_WAS_DONE_PLEASE_FIX_AND_RETRY $s (catch (let $R (eval $s) (py-call (helper.normalize_string $R)))))))))) + (RESULTS: $mcerr))) + ($_ (log INFO "loop" (RESPONSE: $results)))) + (progn (if (or $msgnew (not (== $sexpr ()))) (addToHistory $msg $response $sexpr $msgnew) _) + (ctx-record-command-batch $sexpr $results) + (ctx-maintain-frame) + (change-state! &lastresults (string-safe (repr $results))))) + (if (> (get_time) (get-state &nextWakeAt)) + (change-state! &loops (+ 1 (maxWakeLoops))) + (if (== (get-state &cfv2-root-mode) Fast) (switch-mode) _)))) + + (sleep (sleepInterval)) (cut) (gc) - (omegaclaw (+ 1 $k)))))) + (omegaclaw (+ 1 $k)))) diff --git a/src/memory.metta b/src/memory.metta index 39795f29..64820f8a 100644 --- a/src/memory.metta +++ b/src/memory.metta @@ -26,6 +26,13 @@ (read-file $default_prompt) ""))))) +(= (getContextFramePrompt) + (let $path + (library OmegaClaw-Core ./memory/prompt_context_frame.txt) + (if (exists-file $path) + (read-file $path) + ""))) + (= (getHistory) (let $history_file (library OmegaClaw-Core ./memory/history.metta) @@ -58,4 +65,4 @@ (py-call (lib_chromadb.query (embed $str) (maxRecallItems)))) (= (episodes $time) - (py-call (helper.around_time $time (maxEpisodeRecallLines)))) + (py-call (helper.around_time $time (maxEpisodeRecallLines)))) \ No newline at end of file diff --git a/src/skills.metta b/src/skills.metta index ab30eafd..2d3168e2 100644 --- a/src/skills.metta +++ b/src/skills.metta @@ -78,6 +78,22 @@ (log INFO "skills" (strings-concat ("Remove prompt extension: " $handle))) (collapse (match &self (= (prompt-extension $handle) $text) (remove-atom &self (= (prompt-extension $handle) $text)))) True)) +;; Skills for frame management +(= (contextFramesSkills) + ("CONTEXT_FRAME_MANAGEMENT_SKILLS:" + "- Create top-level frame: new-frame description" (newline) + "- Create low-priority Slow autonomous frame: new-autonomous-frame description" (newline) + "- Toggle Fast/Slow root mode: switch-mode" (newline) + "- Focus an admitted frame: switch-frame frameID" (newline) + "- Extend Slow-mode processing time without going into sleep: send_probe" (newline) + "- Inspect root/current frame: show-root-frame | show-current-frame" (newline) + "- Inspect frame spaces: show-frame-index | show-active-framespace | show-completed-framespace" (newline) + "- Inspect frame relations/dependency of the composed frames: show-frame-relation frameID" (newline) + "- Record hypothesis: ctx-add-hypothesis id hypothesis" (newline) + "- Record result: ctx-add-result variant metrics" (newline) + "- Complete to short-term state: complete-goals-stm summary" (newline) + "- Complete with durable reusable memory: complete-goals-ltm summary" (newline) + "- Clear transient frame data: clear-frame-junk summary")) ; Heartbeat function is called at the start of each iteration of the agentic ; loop. Code can make itself notified about this event using @@ -144,9 +160,18 @@ !(import_prolog_functions_from_file (library OmegaClaw-Core ./src/skills.pl) (shell first_char gc read_file_tail)) -(= (metta $str) - (let $code (sread $str) - (repr (swrite (eval $code))))) - (= (pin $x) PIN-SUCCESS) + +(= (compact-frame $summary) + (progn + (ctx-record-history FrameCompacted $summary) + (change-state! &ctx-history-summary + (last_chars + (py-str + ((get-state &ctx-history-summary) + " | Compact: " + $summary)) + (ctxHistorySummaryLimit))) + (ctx-rebuild-history) + (currentContextFrame)))