feat(loki): give Loki its own in-app tool loop - #214
Merged
Conversation
Loki was a thin proxy to the OpenClaw `main` agent, whose tool root is
~/.openclaw/workspace/. Asked who to contact, it grepped a schema-less
JSON blob there and narrated the incidental matches as attributes —
"Ilya Druzhnikov (UZH)", where UZH is the substring inside drUZHnikov.
FleetCrown's own `people` table, the authoritative one, was invisible.
You cannot constrain an agent whose retrieval you do not own. The
previous PR grounded the CONTEXT; this moves retrieval in-app so the
grounding is actually enforceable.
tools/registry.ts SSOT + defineTool(). Two kinds only: `read` and
`propose`. There is deliberately no `execute` —
Loki still cannot act, it queues drafts and the
operator approves. A test asserts that boundary.
tools/handlers.ts 8 tools over FleetCrown's own tables: people,
projects, pgvector knowledge, goals, habits,
commitments/events, propose_action — and
ask_openclaw, the gateway DEMOTED to one tool
whose output is labelled an unverified
second-hand report.
llm.ts the model seam, with TWO tool protocols.
loop.ts seed → model → tools → facts → verify → repair.
THE INVARIANT: every tool returns Fact[], never prose. A tool result
therefore enters the same contract as everything else — declared fields,
<not recorded> gaps, a citation id, the verifier. There is no channel by
which a tool can put text in front of the model without it also becoming
checkable evidence. ToolResult has no escape hatch; keep it that way.
Dual protocol, because tool calling is where cheap models diverge most.
Native `tools` is advertised; a line protocol (TOOL:/ARGS:) is parsed
from ordinary text on every response regardless, and the two are merged
and deduped. Line-based, not nested JSON, chosen for the weakest model
expected to run it.
That decision is measured, not assumed. `npm run probe:loki-models`
against live Groq: llama-3.3-70b, llama-3.1-8b-instant, qwen3.6-27b,
allam-2-7b and compound-mini ALL drive the loop — and all five did it
via the TEXT protocol, even where native tool calling was advertised
and supported. A native-only parser would have scored zero on every one.
Loop bounds are observed failure modes, not padding: weak models re-call
a tool that already answered, so 3 rounds; the final round advertises no
tools so the model is forced to answer rather than hang; a repair is kept
only if it reduces violations, so the safety pass cannot degrade a turn.
loki-core prefers the loop, falls back to the gateway (still grounded and
verified) on error or LOKI_TOOL_LOOP=0 — an operational escape hatch, not
a feature flag.
Loop deps are imported lazily so the module stays pure to load, which is
what lets scripts/test/agent-tool-loop.ts run with no DB and no API key:
10 checks over protocol tolerance (bolded keys, fenced ARGS, copied
parens, bare calls, trailing prose), the no-execute boundary, fact
accumulation, bad-argument recovery, and bounds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #212. That PR grounded Loki's context; this one moves retrieval in-app so the grounding is actually enforceable.
Why
Loki was a thin proxy to the OpenClaw
mainagent, whose tool root is~/.openclaw/workspace/. Asked who to contact, it grepped a schema-less JSON blob there and narrated incidental matches as attributes — "Ilya Druzhnikov (UZH)", whereUZHis the substring inside drUZHnikov. FleetCrown's ownpeopletable was invisible to it.You cannot constrain an agent whose retrieval you don't own.
What lands
tools/registry.tsdefineTool(). Two kinds only:readandpropose.tools/handlers.tspropose_action, andask_openclaw(the gateway demoted to one tool, output labelled an unverified second-hand report).llm.tsloop.tsThe invariant: every tool returns
Fact[], never prose. A tool result enters the same contract as everything else — declared fields,<not recorded>gaps, a citation id, the verifier. There is no channel by which a tool can put text in front of the model without it also becoming checkable evidence.ToolResulthas no escape hatch.No
executekind exists. Loki still cannot act; it queues drafts and the operator approves. A test asserts that boundary so adding an executing tool fails CI.Dual protocol — measured, not assumed
Tool calling is where cheap models diverge most. Native
toolsis advertised, and a line protocol is parsed from ordinary text on every response regardless:npm run probe:loki-modelsagainst live Groq:Every model — down to 7B — drove the loop, and every one used the TEXT protocol, including models where native tool calling was advertised and supported. A native-only parser would have scored zero on all five. Line-based rather than nested JSON is chosen for the weakest model expected to run it.
Bounds are observed failure modes
3 rounds (weak models re-call a tool that already answered); the final round advertises no tools so the model is forced to answer instead of hanging; a repair is kept only if it reduces violations, so the safety pass cannot degrade a turn.
loki-coreprefers the loop and falls back to the gateway (still grounded and verified) on error orLOKI_TOOL_LOOP=0— an operational escape hatch, not a feature flag.Verification
Loop deps are imported lazily so the module stays pure to load — which is what lets
scripts/test/agent-tool-loop.tsrun with no DB and no API key. 10 checks: protocol tolerance (bolded keys, fenced ARGS, copied parens, bare calls, trailing prose), the no-execute boundary, fact accumulation, bad-argument recovery, bounds. 61/61 unit files pass, tsc clean, 0 lint errors.🤖 Generated with Claude Code