Skip to content

[OMEGA-374] Omega V2: using tools API fields to pass the list of tools to LLM - #349

Open
vsbogd wants to merge 25 commits into
mainfrom
MeTTaClaw2-vitaly
Open

vsbogd wants to merge 25 commits into
mainfrom
MeTTaClaw2-vitaly

Conversation

@vsbogd

@vsbogd vsbogd commented Sep 14, 2026 •

Copy link
Copy Markdown
Member

Description

This is rework of #323 and adopting it to the current Omega code keeping old features in place and adding new features:

  • a set of memory related tools added (memory.metta, skills.metta, prompt.txt) - implemented but removed by 5c1d34f
  • passing list of tools via OpenAI API - implemented by [OMEGA-427] Use tools API to pass the list of tools to the LLM #358
  • keep list of the last messages between LLM and agent in memory (episodic memory?) without saving them to the history.metta file, keep only human messages and tool calls in the history.metta - implemented
  • loop explicitly switches between two modes: "continue work on user's task", "continue autonomous work", "consolidate memory" when no user input - not implemented
  • nop tool which is used by LLM to enter the waiting loop - not implemented

How Has This Been Tested?

I have manually checked the all of LLM providers starting build with each provider and asking "What is the distance to the Sun" each instance of the agent.

All autotests are passed.

Optional autotests are still broken, they need to be updated similarly to the required auto tests.

Checklist

  • PR contains autogenerated code
  • Self-review completed
  • Test scenarios above are passed with the version of the code from PR

patham9 and others added 7 commits September 14, 2026 16:16
@vsbogd
vsbogd requested a review from jazzbox35 September 14, 2026 14:22
@vsbogd

vsbogd commented Sep 14, 2026 •

Copy link
Copy Markdown
Member Author

Command to run dev build:
curl -fsSL https://raw.githubusercontent.com/singnet/Omega/refs/heads/MeTTaClaw2-vitaly/scripts/omega | bash -s -- singularitynet/omega:6e9f0fbb944b64d15a4ee5b668be874619d9378a

@vsbogd
vsbogd marked this pull request as ready for review September 16, 2026 14:11
@vsbogd

vsbogd commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

@pisaev1 could you please review this, unfortunately I cannot add you as a reviewer probably because you didn't contribute to the repo but you should be able to publish comments here.

@vsbogd

vsbogd commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

CI job fails to start tests for some reason will look at it. Should not be blocker for the review as I ran integration tests locally and published the results in the description.

@vsbogd

vsbogd commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

@patham9 @pisaev1 pin tool is broken in #323 because it is never sent to the LLM in the list of last messages as it was before. Now it is present only in history.metta file. This breaks the integration test mock/test_pin_invisible_within_iteration_mock.py. I am not sure was this change made by intention or accidentally?

@vsbogd

vsbogd commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

@pisaev1 @patham9 could you please also answer questions I asked in https://github.com/singnet/Omega/pull/323/changes because they are critical for the understanding the OmegaV2 changes.

@jazzbox35

Copy link
Copy Markdown
Collaborator

Overall it seems to run pretty well in my tests and aesthetically seems well organized!

Here are some suggestions and question:

  1. I feel like making the main loop the last / lowest function inside loop.metta would help readability. I'm used to the loop being at the bottom and I think people can orient around the bottom of the program to reference functions above the main loop.

  2. I see in callTools that we are calling each tool in the sequence received. I wonder if this could be done with a superpose or hyperpose structure? I'm not sure though if parallelism or nondeterminism is supported with respect to tool calls or even if we must execute tool calls in sequence. I don't know if there is an implied or explicit reason to make tool calls sequential or parallel...

  3. I think the new loop.py program is what Patrick devised as the base of version 2 (?). It looks like this can then be run as a way of running Omega starting with Python. I'm not sure this is necessary and I worry that a loop.py program could be out of sync with a loop.metta program. Maybe we should omit loop.py if it is just doing the same thing as loop.metta? We might be stuck updating two algorithms for a single problem right? (I think I understand this).

  4. In a couple of instances when running tests, I received strange output in my chat channel, once an announcement that an auction was coming up and another case of two numbers appearing. If this happens again I will save the log.

I think both @blackhammer116 and @patham9 should also review this given the magnitude of the release, and also that everybody is encouraged to "test drive" version 2.

@vsbogd

vsbogd commented Sep 16, 2026

Copy link
Copy Markdown
Member Author
  1. I feel like making the main loop the last / lowest function inside loop.metta would help readability. I

No problem with that. I will do this change tomorrow.

  1. I see in callTools that we are calling each tool in the sequence received. I wonder if this could be done with a superpose or hyperpose structure?

We cannot rely on the order of execution provided by superpose it can be random.

I don't know if there is an implied or explicit reason to make tool calls sequential or parallel...

Autotests rely on the sequence of execution of the tool calls sent. I believe LLM will also form a list of calls for being executed in sequence.

  1. Maybe we should omit loop.py if it is just doing the same thing as loop.metta?

Absolutely agree, we need to remove it. I didn't do this yet because there are some features to copy as the next step.

  1. In a couple of instances when running tests, I received strange output in my chat channel, once an announcement that an auction was coming up and another case of two numbers appearing.

Yes, log would be very helpful here to understand if this was triggered by something on agent side.

@MartinEbner

Copy link
Copy Markdown

Thanks for picking up the loop rework. One caching point while the design is still open, and a
small nit.

episodicTail trims the trace to the last episodicTraceSize entries and writes the result back
into &episodic, and episodicGet is what supplies the messages of the request. Once the trace is
full, each turn drops the oldest entries, so the first entry in it is a different one on every
call. Providers reuse a cached prefix only while it is byte-identical, so the reusable prefix stops
where the trace starts and the whole trace is uncached input on every iteration.

That is #300 in the new shape: there it is HISTORY as a sliding byte window over an append-only
file, here it is the episodic trace as a sliding message window. Measured on the current layout
against a real 2.4 MB history.metta, replaying 300 consecutive cycles: 4.2 KB of a 58 KB prompt
stayed byte-identical between calls, about 7%.

The fix is cheap and costs nothing in what the model sees: drop in blocks rather than one entry at
a time. Let the trace grow to episodicTraceSize + K and then drop K at once. The head then stays
put until K further entries have accumulated, instead of moving on every call, the trace length
varies between N and N+K, and the cacheable prefix covers everything up to the newest block. On the
byte-window version of this we found a block of about a sixth of the window to be a good starting
point, which would be K ≈ 17 here: small enough that the trace stays close to its nominal size,
large enough that the head survives a good many calls.

One note for episodicCutToolResults when it is implemented: if it rewrites earlier tool entries
in place each turn, that invalidates the prefix again for the same reason. Truncating a result
once, as it leaves the recent zone, avoids it.

Nit: in the let* chain, $recent_messages is bound to (episodicTail …) but never used — the
trimming happens through its side effect on &episodic, and the request reads (episodicGet).
Worth either using the binding or calling it for effect, so the dependency is visible.

Happy to open the block-drop change as a small PR against this branch if that is useful.

@vsbogd

vsbogd commented Sep 21, 2026

Copy link
Copy Markdown
Member Author

The fix is cheap and costs nothing in what the model sees: drop in blocks rather than one entry at a time. Let the trace grow to episodicTraceSize + K and then drop K at once.

Thanks @MartinEbner it is a brilliant idea. I think we definitely going to implement this.

One note for episodicCutToolResults when it is implemented: if it rewrites earlier tool entries in place each turn, that invalidates the prefix again for the same reason. Truncating a result once, as it leaves the recent zone, avoids it.

Yes, I agree I am not even sure the truncating is needed and at the moment it is not implemented. Anyway thanks for this, I think we just need to evaluate it carefully before implementing.

Nit: in the let* chain, $recent_messages is bound to (episodicTail …) but never used — the trimming happens through its side effect on &episodic, and the request reads (episodicGet). Worth either using the binding or calling it for effect, so the dependency is visible.

Totally agree, thanks for noticing this. I will remove $recent_messages.

Happy to open the block-drop change as a small PR against this branch if that is useful.

Ah, don't bother, at the moment I think about reworking this PR after talking with @pisaev1 and I will incorporate your comments.

@TossSky

TossSky commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

@vsbogd @MartinEbner @jazzbox35 @patham9
Tested: image built from 9d28e83, Omega version=v0.1.19-162-g9d28e83. The CI suites ran with the agent started as in CI, the WebSocket and Slack mock suites on their own channels, and the live IRC suite four times, once per provider: Anthropic claude-opus-4-8, OpenRouter z-ai/glm-5.2, OpenAI gpt-5.5 and ASICloud minimax/minimax-m3.

What I checked:

  • tests/mettatest.sh: 6 of 6 OK
  • tests/pytest.sh: 75 passed, 1 skipped
  • pytest @run_mandatory: 151 passed
  • pytest @run_optional: 5 passed, 1 skipped
  • import_knowledge/ with OMEGA_KB_IMAGE set: 21 passed
  • mock_websocket/: 6 passed, 7 failed. The 7 failures are the end-to-end tests. Their mock answers are still strings, which the new mock turns into an empty response, so the agent does nothing (item 7).
  • mock_slack/: 5 passed, 17 failed, 1 skipped, 2 errors. 15 of the failures are tests that still use string answers. test_memory_history_slack_mock and test_search_invalid_slack_mock already use the new format and fail because the mock can't find their prompt behind Slack's sender prefix. The errors are test_skill_query_slack_mock.py, which doesn't parse, and test_slack_unwrap.py, whose import of channels.slack fails on main too.
  • live IRC suite with Anthropic: 33 passed, 2 failed, 1 skipped
  • live IRC suite with OpenAI: 33 passed, 2 failed, 1 skipped
  • live IRC suite with OpenRouter: 18 passed, 17 failed, 1 skipped
  • live IRC suite with ASICloud: 20 passed, 15 failed, 1 skipped

The skipped test in every run is the git push test, which needs OMEGA_GIT_TOKEN. On Anthropic and OpenAI the two failures are test_create_script.py and test_complex_weather_flow.py, and both tests are the same on main. The first can't pass because the launcher mounts /tmp without exec (scripts/omega:804), so the script the agent writes there can't run. The second looks for the write-file of p.sh in a list taken before p.sh appears (line 84) and checks its mode once, before the agent runs chmod +x (line 105). ASIOne and the generic OpenAIAPI provider were not tested, and Slack, Telegram and WebSocket were tested only with the mock.

  1. A read-file call for a file that doesn't exist stops the agent. The call is logged twice and PeTTa's main thread exits with code 2:

    ERROR: [Thread main] /PeTTa/src/main.pl:23: user:main open/3: source_sink `'/PeTTa/src/../lib/../Omega/./memory/history.metta'' does not exist (No such file or directory)
    

    The path is ./memory/history.metta under the Omega library alias, as written in memory.metta:31 and :45, but resolved under /PeTTa/lib. A single mocked answer [("read-file", {"filename": "/tmp/does-not-exist.txt"})] reproduces it, and so does an empty file name. On main at 6657798 the same call leaves the agent running. In the live OpenRouter run the model returned a batch of calls with empty arguments. The read-file "" among them stopped the agent, and every later test failed with container ... is not running.

  2. The episodic window is trimmed with a plain slice (episodicTail), so it can start with a tool result whose call has been cut off. Anthropic, OpenAI and ASICloud reject such requests, and OpenRouter accepts them:

    Anthropic    23 of 340 requests   'tool_call_id' of 'toolu_...' not found in 'tool_calls' of previous message.
    OpenAI       57 of 347 requests   No tool call found for function call output with call_id fc_...
    ASICloud     37 of 741 requests   Message has tool role, but there was no previous assistant message with a tool call!
    OpenRouter    0 of 129 requests
    

    The first rejection comes at iteration 34 on Anthropic and 31 on OpenAI, about two and a half minutes after the agent started.

  3. Rejected requests are dropped silently. The error goes to &temporary_message (loop.metta:137), which nothing reads. The next iteration starts from the same state, so neither the model nor the user learns about the rejection.

  4. The fallback for replies cut off by the token limit doesn't work on two providers. openai.py:136 calls _llm_empty_response_tool_call without the llm. prefix. The function is defined in lib_llm_ext.py:66, so a Responses API reply cut off by max_output_tokens raises NameError: name '_llm_empty_response_tool_call' is not defined. The fallback call id is f"{response_id}#{uuid.uuid4().hex}" (lib_llm_ext.py:71). When a turn with this id goes back to the provider, Anthropic rejects it (tool_use.id: String should match pattern '^[a-zA-Z0-9_-]+$') and OpenAI rejects the 86-character call_id (maximum length 64). OpenRouter accepts the id, and in its run the fallback fired 7 times.

  5. An unknown tool name crashes validation. _validate_response calls set_error and then reads call.tool.parameters on None, and llmProviderChat swallows the AttributeError and returns an empty response. This happened in the green mock run, in test_unload_removes_skill, which passes because it only checks that the unloaded skill produces no output.

  6. Delegation results are appended to history.metta (openclaw.metta:56), and the loop never puts that file into the model's context. In the mock run the model saw an OPENCLAW_RESULT line once, inside an episodes result requested by another test. The skill description still says the reply arrives on its own.

  7. The channel suites that CI doesn't run were not moved to the new mock format. 16 files in mock_slack/, all 24 in mock_telegram/ and 7 in mock_websocket/ still pass the answer as a string like '(send "...")', and the mock turns that into an empty response. The mock's prompt lookup also strips only up to the first ": ". With the new Step <ts>: prefix in front, that leaves Slack's sender prefix in place, so on Slack an answer is found only when two messages arrive in one iteration. test_skill_query_slack_mock.py:47 doesn't parse. I also opened [OMEGA-429] Update channel mock suites for the V2 mock #363 against this branch. It moves these tests to the new format and strips the prefix in the mock. With it applied, mock_websocket/ passes 13 of 13, mock_telegram/ passes 23 with 1 skipped, and mock_slack/ passes 32 with 1 skipped. The remaining Slack test, test_git_pull_public_slack_mock, failed once when the shell time limit cut git clone short and passed on two reruns.

  8. llmToolCallToSExpr escapes " but not \ (providers.py:316), so ls C:\ becomes (shell "ls C:\") and the backslash escapes the closing quote. A literal \n in an argument turns into a newline. A non-string argument raises AttributeError at loop.metta:79, outside the catch on line 83.

  9. providers.py:2 imports Self from typing, which needs Python 3.11. Autotests/mock/llm.py imports this module, so on 3.10 the Autotests conftest fails to load, while the README says 3.10 or later.

Verdict: FAIL

@vsbogd

vsbogd commented Sep 25, 2026

Copy link
Copy Markdown
Member Author

Thanks @TossSky it is very helpful.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants