Skip to content

feat: subagent dispatch (delegate skill)#137

Open
hlgreenblatt wants to merge 1 commit into
asi-alliance:mainfrom
hlgreenblatt:upstream-pr/subagent-dispatch
Open

feat: subagent dispatch (delegate skill)#137
hlgreenblatt wants to merge 1 commit into
asi-alliance:mainfrom
hlgreenblatt:upstream-pr/subagent-dispatch

Conversation

@hlgreenblatt

@hlgreenblatt hlgreenblatt commented May 24, 2026

Copy link
Copy Markdown

Summary

Adds (delegate "<goal>") — a bounded child-LLM loop that runs against a
configurable provider/model/endpoint (per-persona JSON config) and returns
a single-string digest to the parent. Lets a deployment pair its
foundation-model parent (good at parsing intent + routing) with narrow
specialist subagents (often smaller, cheaper, or local) that execute
defined sub-tasks within tight tool budgets.

The persona config (memory/personas-subagent/<key>.json) names a
prompt file plus a (provider, model, base_url, api_key_env) tuple,
so different personas can bind to different LLM endpoints. The
dispatcher constructs a fresh AIProvider (from lib_llm_ext) per
call — stays inside the existing class abstraction, does not mutate
_provider_registry.

Architecture, in one sentence

Right-sized model for the task, foundation model for the routing.
The architectural payoff (sovereignty, specialist quality, marginal
cost approaching zero on local subagents) matters independently of
the token math; the token math (below) validates that the
plumbing actually delivers the claimed shape.

Measured

End-to-end verification ran against a working OmegaClaw deployment
dispatching to a local Ollama-served Granite 4.1 30B specialist over
the (delegate "what is 7 plus 13") test goal.

Per-call median input tokens Per-call ratio
Parent (frontier-tier API) ~15,000
Subagent (local Ollama specialist) ~500 30.5× smaller per iteration

Counterfactual workload analysis (same task without dispatch — parent
would run the 13 iterations itself): ~195K parent input tokens vs.
~41K with dispatch = ~4.7× workload-token reduction same-model.
Multiplied by the per-token price ratio when the parent is a frontier
API and the subagent is local Ollama: order-of-magnitude per-workload
cost reduction.

What's in this PR

7 files, +1,106 / -0 lines, all additive. No changes to the agent
loop, lib_llm_ext, or any existing skill body.

File Change
src/subagent.py (new) The dispatch primitive — JSON persona loader, tool-subset parser with v1-exclusion check, fresh AIProvider per dispatch, mini-loop with strict (emit "<digest>") termination, response-cleanup (<think> strip, fence strip, line-leading s-expr parse), tool registry: search, read-file, write-file, append-file, restricted shell, plus tavily-search / technical-analysis when uagents is available. ~660 lines.
src/skills.metta +26 lines: catalog entry in getSkills + three positional rules (1-arg, 3-arg, 4-arg). 1-arg form matches OmegaClaw's single-arg-per-skill convention; 3- and 4-arg forms for explicit persona / max-turns control.
memory/personas-subagent/.gitkeep (new) Directory marker. Persona configs are per-deployment; the repo ships no example bindings.
memory/personas-subagent/README.md (new) JSON config schema + security rule (never embed keys, always reference an env var by name) + tool taxonomy.
memory/personas-subagent/prompt-researcher.txt (new) Example persona prompt — short (~1 KB), declarative, explicit (emit "...") protocol.
docs/reference-skills-subagent.md (new) Skill reference: signature, parameters, return shape, configuration env vars, failure-mode table. Same shape as docs/reference-skills-remote-agents.md.
docs/tutorial-09-subagents.md (new) End-to-end walkthrough — persona prompt + config + dispatch + reading the digest. Same shape as docs/tutorial-03-writing-a-custom-skill.md.

Configuration

Three optional env vars, all with safe defaults:

Env var Default Meaning
OMEGACLAW_SUBAGENT_PERSONA_DIR ./memory/personas-subagent Directory holding <key>.json + persona prompt files.
OMEGACLAW_SUBAGENT_MAX_TURNS 8 Hard cap on subagent iterations per dispatch.
OMEGACLAW_SUBAGENT_MAX_DIGEST_CHARS 2000 Length cap on the string returned to the parent.

v1 scope and explicit exclusions

Tools the subagent cannot call in v1:

  • remember, query, episodes — would couple the subagent into the parent's memory state. Subagents are stateless; v2 may add a tagged write variant.
  • pin, metta — mutate parent atomspace state.
  • send — would let the subagent talk to the user channel directly, defeating the digest-return invariant.
  • delegate — no subagent → subagent recursion in v1.

These are enforced at dispatch time: any v1-excluded skill in the
tools argument returns a structured error string and the subagent
doesn't run.

No new dependencies

The dispatcher uses Python stdlib + the existing openai package
(via lib_llm_ext.AIProvider). JSON persona configs use stdlib
json. No pyyaml. No new entry in requirements.txt.

Test plan

  • python3 -m py_compile src/subagent.py — passes
  • python3 -c "import sys; sys.path.insert(0,'src'); import subagent" — passes (without lib_llm_ext available, since the import is deferred to dispatch time)
  • src/skills.metta parens balanced (verified: 99 opens, 99 closes)
  • Persona config error path: missing JSON file returns (subagent error: persona config '<key>.json' not found...)
  • v1-excluded tool error path: (delegate "x" "remember" "researcher" 4) returns (subagent error: skill(s) ['remember'] are not callable by subagents in v1...)
  • Unknown tool error path: similar
  • End-to-end dispatch against a configured Ollama-local persona (deployment-side; the repo ships no live persona bindings)

Anti-claims

This PR does not:

  • Make small specialist models good at tasks they weren't trained for. Parent's routing judgment is the load-bearing piece.
  • Eliminate the parent's per-turn cost. Subagents replace the delegated work; the parent still pays for the routing + integration turns.
  • Solve cross-deployment subagent sharing. Each deployment configures its own persona binding.
  • Touch the existing agent loop, lib_llm_ext, or any skill body. Purely additive.

Related — one-line dependency

The subagent's delegate dispatch returns a string via py-call,
which means it surfaces a pre-existing bug in src/loop.metta:65:
the safety-net check (== "(" (first_char $resp)) fails on
String-vs-atom type mismatch for valid Python-string returns from
py-call, causing the agent's actual output to be substituted for
the reminder string. The bug is documented in a downstream fork's
implementation notes and the one-line fix is well-tested:

-  ($response (if (== "(" (first_char $resp)) $resp (progn (println! $resp) (repr (REMEMBER:OUTPUT_NOTHING_ELSE_THAN: ((skill arg) ...))))))
+  ($response (if (> (string_length $resp) 1) $resp (progn (println! $resp) (repr (REMEMBER:OUTPUT_NOTHING_ELSE_THAN: ((skill arg) ...))))))

Now open as #138 for independent review and merge ordering. The
fix is unrelated to subagent functionality and benefits any new
py-call-bridged skill. The subagent dispatch in this PR will
hit the same bug if #138 doesn't land first.


Architectural framing + measurement methodology in
docs/reference-skills-subagent.md and docs/tutorial-09-subagents.md.

Adds the (delegate "<goal>") skill — a bounded child-LLM loop that
runs against a configurable provider/model/endpoint (per-persona
JSON config) and returns a single-string digest to the parent.
Lets a deployment pair its foundation-model parent with narrow
specialist subagents (often smaller, cheaper, or local) that
execute defined sub-tasks within tight tool budgets.

Implementation in src/subagent.py is self-contained — Python stdlib
plus lib_llm_ext.AIProvider (instantiated fresh per dispatch from
the persona's JSON config; does not mutate _provider_registry).
JSON config schema documented at memory/personas-subagent/README.md.
Per-persona binding lets one deployment route different sub-tasks
to different models (e.g. local Ollama for code, frontier API
for synthesis).

v1 tool registry: search, read-file, write-file, append-file,
shell (restricted — no apostrophes, 30s timeout, 4 KB output cap),
plus tavily-search and technical-analysis when uagents is
importable. v1 excludes remember, query, episodes, pin, metta,
send, delegate — enforced at dispatch time with a structured
error string back to the parent.

Three forms of (delegate ...) in src/skills.metta:
- 1-arg (delegate "<goal>")              — matches OmegaClaw's
                                           single-arg-per-skill
                                           convention; uses
                                           "researcher" persona
- 3-arg (delegate "<g>" "<tools>" "<p>") — explicit persona,
                                           default max_turns=8
- 4-arg (delegate "<g>" "<tools>" "<p>" <n>) — full control

Measured against a working deployment dispatching to a local
Ollama-served Granite 4.1 30B specialist for the test goal
"what is 7 plus 13":

  Parent (frontier API):   ~15,000 input tokens / call median
  Subagent (local Ollama): ~500 input tokens / call median
  Per-call ratio:          30.5× (median), 17.6× (mean)
  Workload reduction:      ~4.7× same-model (modeled
                           counterfactual: parent runs all 13
                           iterations itself = ~195K tokens; with
                           dispatch = ~41K tokens)

Three new env vars, all with defaults — see
docs/reference-skills-subagent.md§Configuration:
  OMEGACLAW_SUBAGENT_PERSONA_DIR
  OMEGACLAW_SUBAGENT_MAX_TURNS
  OMEGACLAW_SUBAGENT_MAX_DIGEST_CHARS

Zero changes to the agent loop, lib_llm_ext, or any existing skill
body. Zero new pip dependencies.

7 files, +1106 / -0 lines:
  src/subagent.py                                  (new, 663 lines)
  src/skills.metta                                 (+26 lines: catalog + 3 rules)
  memory/personas-subagent/.gitkeep                (new, marker)
  memory/personas-subagent/README.md               (new, schema)
  memory/personas-subagent/prompt-researcher.txt   (new, example)
  docs/reference-skills-subagent.md                (new, reference)
  docs/tutorial-09-subagents.md                    (new, walkthrough)
@vsbogd vsbogd added the plugin label Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants