mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
659 lines
24 KiB
Markdown
659 lines
24 KiB
Markdown
# Agent / Subagent / Tool Flow
|
||
|
||
This document explains the current runtime flow around the agent harness, with emphasis on:
|
||
|
||
- how the main agent turn executes
|
||
- how tools are exposed and executed
|
||
- how `spawn_subagent` works
|
||
- how typed vs fork subagents differ
|
||
- where to look when debugging harness and delegation issues
|
||
|
||
Scope: current Rust implementation under `src/openhuman/agent/` and `src/openhuman/tools/`.
|
||
|
||
## Why This Exists
|
||
|
||
The code path is split across several layers:
|
||
|
||
- built-in agent definitions in `src/openhuman/agent/agents/`
|
||
- harness data + task-local plumbing in `src/openhuman/agent/harness/`
|
||
- main session lifecycle in `src/openhuman/agent/harness/session/`
|
||
- delegation tools in `src/openhuman/agent/tools/`
|
||
- synthesised `delegate_*` tools in `src/openhuman/tools/orchestrator_tools.rs`
|
||
|
||
If you only read one file, the system looks simpler than it is. The actual runtime path crosses all of them.
|
||
|
||
## File Map
|
||
|
||
### Registry and definitions
|
||
|
||
- `src/openhuman/agent/agents/loader.rs`
|
||
Loads built-in agents from `agent.toml` plus dynamic `prompt.rs` builders.
|
||
- `src/openhuman/agent/harness/definition.rs`
|
||
Defines `AgentDefinition`, `ToolScope`, `SubagentEntry`, `PromptSource`, and registry-facing data.
|
||
- `src/openhuman/agent/harness/mod.rs`
|
||
Re-exports the harness entrypoints.
|
||
|
||
### Main agent session
|
||
|
||
- `src/openhuman/agent/harness/session/builder.rs`
|
||
Builds an `Agent`, chooses dispatcher, applies visible-tool filtering, synthesises delegation tools.
|
||
- `src/openhuman/agent/harness/session/turn.rs`
|
||
Main turn lifecycle, tool execution, parent/fork context setup, transcript persistence, post-turn hooks.
|
||
|
||
### Subagent path
|
||
|
||
- `src/openhuman/agent/tools/spawn_subagent.rs`
|
||
Runtime tool entrypoint for explicit subagent spawns.
|
||
- `src/openhuman/agent/harness/fork_context.rs`
|
||
Task-local parent and fork context.
|
||
- `src/openhuman/agent/harness/subagent_runner.rs`
|
||
Typed/fork subagent execution, inner loop, tool filtering, transcript writes, large-result handoff.
|
||
|
||
### Generic tool loop / bus path
|
||
|
||
- `src/openhuman/agent/harness/tool_loop.rs`
|
||
Shared LLM -> tool -> tool result -> LLM loop used by the bus handler and legacy call sites.
|
||
- `src/openhuman/agent/bus.rs`
|
||
Native event-bus entrypoint `agent.run_turn`.
|
||
|
||
## High-Level Model
|
||
|
||
There are two related but distinct execution tiers:
|
||
|
||
1. `Agent::turn`
|
||
This is the stateful session runtime. It owns conversation history, system prompt reuse, memory loading, hooks, transcript resume, and the parent context needed for subagents.
|
||
|
||
2. `run_subagent`
|
||
This is an isolated delegated run. It does not become a nested full `Agent` session. It runs a smaller inner loop and returns a single compact text result to the parent as a normal tool result.
|
||
Every typed subagent prompt now also appends a shared "Sub-agent Role Contract" suffix that explicitly states sub-agent role expectations and requires concise, synthesis-ready outputs.
|
||
|
||
That distinction matters when debugging. A subagent is not a second copy of the full session runtime.
|
||
|
||
## Flow Diagram
|
||
|
||
### Full parent -> tool -> subagent flow
|
||
|
||
```text
|
||
User message
|
||
|
|
||
v
|
||
+---------------------------+
|
||
| Agent::turn |
|
||
| session/turn.rs |
|
||
+---------------------------+
|
||
|
|
||
| 1. resume transcript if present
|
||
| 2. build/reuse system prompt
|
||
| 3. load memory context
|
||
| 4. install ParentExecutionContext task-local
|
||
v
|
||
+---------------------------+
|
||
| Parent iteration loop |
|
||
| provider call |
|
||
+---------------------------+
|
||
|
|
||
| provider response
|
||
v
|
||
+---------------------------+
|
||
| Parse tool calls |
|
||
| dispatcher + parser |
|
||
+---------------------------+
|
||
|
|
||
+-------------------------------+
|
||
| no tool calls |
|
||
| |
|
||
v |
|
||
+---------------------------+ |
|
||
| Final assistant text | |
|
||
| appended to parent history| |
|
||
+---------------------------+ |
|
||
| |
|
||
v |
|
||
Return to caller |
|
||
|
|
||
| has tool calls
|
||
v
|
||
+---------------------------+
|
||
| Execute tool calls |
|
||
| parent tool runtime |
|
||
+---------------------------+
|
||
|
|
||
+-------------------+-------------------+
|
||
| |
|
||
| regular tool | spawn_subagent
|
||
v v
|
||
+---------------------------+ +---------------------------+
|
||
| Tool::execute(...) | | SpawnSubagentTool |
|
||
+---------------------------+ | impl/agent/ |
|
||
| | spawn_subagent.rs |
|
||
| result +---------------------------+
|
||
v |
|
||
+---------------------------+ | validate args
|
||
| append tool result | | lookup AgentDefinition
|
||
| to parent history | | publish spawn event
|
||
+---------------------------+ v
|
||
| +---------------------------+
|
||
+-------------------------->| run_subagent(...) |
|
||
| subagent_runner.rs |
|
||
+---------------------------+
|
||
|
|
||
+-------------------------+-------------------------+
|
||
| |
|
||
| typed mode | fork mode
|
||
v v
|
||
+---------------------------+ +---------------------------+
|
||
| run_typed_mode | | run_fork_mode |
|
||
| - resolve model | | - require ForkContext |
|
||
| - filter tools | | - replay parent prefix |
|
||
| - build narrow prompt | | - reuse parent tool specs |
|
||
+---------------------------+ +---------------------------+
|
||
| |
|
||
+-------------------------+-------------------------+
|
||
|
|
||
v
|
||
+---------------------------+
|
||
| run_inner_loop |
|
||
| subagent private loop |
|
||
+---------------------------+
|
||
|
|
||
+---------------------------+---------------------------+
|
||
| |
|
||
| no tool calls | tool calls
|
||
v v
|
||
+---------------------------+ +---------------------------+
|
||
| final child text | | child executes allowed |
|
||
| returned to parent tool | | tools, appends results, |
|
||
+---------------------------+ | loops again |
|
||
+---------------------------+
|
||
|
|
||
v
|
||
+---------------------------+
|
||
| SpawnSubagentTool returns |
|
||
| ToolResult(output) |
|
||
+---------------------------+
|
||
|
|
||
v
|
||
+---------------------------+
|
||
| parent appends tool |
|
||
| result to history |
|
||
+---------------------------+
|
||
|
|
||
v
|
||
+---------------------------+
|
||
| next parent iteration |
|
||
| synthesizes final answer |
|
||
+---------------------------+
|
||
```
|
||
|
||
### Context wiring for subagents
|
||
|
||
```text
|
||
Agent::turn
|
||
|
|
||
+--> build ParentExecutionContext
|
||
| - provider
|
||
| - all_tools / all_tool_specs
|
||
| - model / temperature
|
||
| - memory / memory_context
|
||
| - connected_integrations
|
||
| - composio_client
|
||
| - tool_call_format
|
||
| - session lineage
|
||
|
|
||
+--> with_parent_context(...)
|
||
|
|
||
+--> any tool call inside this turn can read current_parent()
|
||
|
|
||
+--> SpawnSubagentTool
|
||
|
|
||
+--> run_subagent(...)
|
||
|
|
||
+--> typed mode uses ParentExecutionContext directly
|
||
|
|
||
+--> fork mode also requires current_fork()
|
||
|
|
||
+--> exact parent prompt + prefix replay
|
||
```
|
||
|
||
## Startup and Registry Loading
|
||
|
||
Built-in agents live under `src/openhuman/agent/agents/*/` as:
|
||
|
||
- `agent.toml`
|
||
- `prompt.rs`
|
||
- optional `prompt.md` kept as nearby reference material
|
||
|
||
`loader.rs` parses each `agent.toml`, stamps the source as builtin, and installs the `prompt.rs` builder as `PromptSource::Dynamic`.
|
||
|
||
The global `AgentDefinitionRegistry` is initialized at startup. `spawn_subagent` depends on it. If the registry is missing, the tool returns a clear error instead of trying to run.
|
||
|
||
Important consequence: agent delegation is data-driven. The runtime does not hardcode an enum of built-in agents.
|
||
|
||
## How a Main Agent Session Is Built
|
||
|
||
`AgentBuilder::build` in `session/builder.rs` assembles:
|
||
|
||
- provider
|
||
- full tool registry
|
||
- visible tool specs
|
||
- memory backend
|
||
- prompt builder
|
||
- dispatcher
|
||
- context manager
|
||
|
||
Two tool sets exist at build time:
|
||
|
||
- full tool registry: what the runtime can execute
|
||
- visible tool set: what the model can see in its schema/prompt
|
||
|
||
That split is intentional. The parent may have access to more runtime tools than it exposes directly to the model.
|
||
|
||
### Synthesised delegation tools
|
||
|
||
For agents with `subagents = [...]` in their definition, the builder synthesises `delegate_*` tools using `collect_orchestrator_tools()`:
|
||
|
||
- `SubagentEntry::AgentId("researcher")` becomes an `ArchetypeDelegationTool`
|
||
- `SubagentEntry::Skills({ skills = "*" })` expands to one `SkillDelegationTool` per connected integration
|
||
|
||
These tools are added to the model-visible surface at build time. They are wrappers around delegation, not standalone business logic.
|
||
|
||
## Main Turn Flow
|
||
|
||
`Agent::turn` in `session/turn.rs` is the main harness path.
|
||
|
||
### 1. Transcript resume and prompt bootstrap
|
||
|
||
On a fresh session:
|
||
|
||
- it tries to resume a previous transcript for KV-cache reuse
|
||
- fetches connected integrations
|
||
- fetches learned context
|
||
- builds the system prompt once
|
||
- stores that system prompt as the first message
|
||
|
||
On later turns it deliberately does not rebuild the system prompt. Byte stability is treated as a runtime invariant for backend prefix caching.
|
||
|
||
### 2. Memory context injection
|
||
|
||
Per turn, it asks the memory loader for relevant context and prepends that context to the user message. This is parent-session behavior. Subagents do not run the same memory lookup path.
|
||
|
||
### 3. Parent execution context is captured
|
||
|
||
Before the loop starts, `Agent::turn` snapshots a `ParentExecutionContext` and installs it on the task-local via `with_parent_context(...)`.
|
||
|
||
That context carries the data subagents need:
|
||
|
||
- provider
|
||
- all tools and tool specs
|
||
- model / temperature
|
||
- memory handle
|
||
- loaded memory context
|
||
- connected integrations
|
||
- composio client
|
||
- tool call format
|
||
- session / transcript lineage
|
||
|
||
Without this task-local, `spawn_subagent` cannot work.
|
||
|
||
### 4. Iterative provider loop
|
||
|
||
For each iteration:
|
||
|
||
- context reduction runs first
|
||
- the dispatcher converts history into provider messages
|
||
- the provider is called
|
||
- response text and tool calls are parsed
|
||
- tool calls are executed
|
||
- tool results are appended to history
|
||
- the loop repeats until no tool calls remain
|
||
|
||
This is the full parent loop. It also emits progress events and drives post-turn hooks.
|
||
|
||
## Tool Execution in the Parent Loop
|
||
|
||
The parent loop special-cases delegation but otherwise treats tools generically.
|
||
|
||
Core behaviors:
|
||
|
||
- unknown or filtered-out tools become structured error results
|
||
- `CliRpcOnly` tools are blocked in the autonomous loop
|
||
- approval-gated tools can be denied before execution
|
||
- successful outputs may be scrubbed / compacted / summarized
|
||
|
||
The parent’s history preserves:
|
||
|
||
- assistant tool call intent
|
||
- tool results
|
||
- final assistant response
|
||
|
||
That history format is what the next iteration reasons from.
|
||
|
||
## Where `spawn_subagent` Enters
|
||
|
||
The explicit delegation tool lives in `src/openhuman/agent/tools/spawn_subagent.rs`.
|
||
|
||
Its flow is:
|
||
|
||
1. parse `agent_id`, `prompt`, optional `context`, optional `toolkit`, optional `mode`
|
||
2. require the global `AgentDefinitionRegistry`
|
||
3. resolve the target definition
|
||
4. run pre-flight validation for `integrations_agent`
|
||
5. publish `DomainEvent::SubagentSpawned`
|
||
6. call `run_subagent(...)`
|
||
7. publish completed or failed event
|
||
8. return the subagent’s final text as a normal `ToolResult`
|
||
|
||
Important: the parent model never sees the subagent’s internal transcript. It only sees the final tool result string returned by `spawn_subagent`.
|
||
|
||
## Typed vs Fork Subagents
|
||
|
||
`run_subagent` chooses one of two modes.
|
||
|
||
### Typed mode
|
||
|
||
Default path. Implemented by `run_typed_mode(...)`.
|
||
|
||
Behavior:
|
||
|
||
- resolves model from the definition
|
||
- filters the parent’s tools down to what the child is allowed to use
|
||
- builds a fresh narrow system prompt
|
||
- optionally injects inherited memory context
|
||
- runs an isolated inner tool loop
|
||
|
||
This is the normal specialist-agent path.
|
||
|
||
### Fork mode
|
||
|
||
Optimization path. Implemented by `run_fork_mode(...)`.
|
||
|
||
Behavior:
|
||
|
||
- requires a `ForkContext` task-local
|
||
- replays the parent’s exact rendered prompt and exact message prefix
|
||
- reuses the parent’s tool schema snapshot
|
||
- appends only the new fork task prompt
|
||
- runs the same inner loop
|
||
|
||
This is for prefix-cache reuse, not for stricter isolation. It is deliberately byte-stable and closely coupled to the parent request shape.
|
||
|
||
## How Tool Filtering Works for Subagents
|
||
|
||
Typed subagents do not get a cloned tool registry. Instead the runner filters the parent’s tool list by index.
|
||
|
||
Filtering inputs:
|
||
|
||
- `definition.tools`
|
||
- `definition.disallowed_tools`
|
||
- `definition.skill_filter`
|
||
- `SubagentRunOptions.skill_filter_override`
|
||
- `definition.extra_tools`
|
||
|
||
Additional runtime rules:
|
||
|
||
- non-`welcome` subagents lose `complete_onboarding`
|
||
- `tools_agent` strips Composio skill tools
|
||
- `integrations_agent` with a bound toolkit may inject dynamic per-action Composio tools
|
||
|
||
The allowed tool names become both:
|
||
|
||
- the execution allowlist
|
||
- the prompt-visible tool catalog
|
||
|
||
If the model emits a tool call outside that allowlist, the runner feeds back an error result and continues.
|
||
|
||
## Prompt Construction for Typed Subagents
|
||
|
||
Typed mode creates a `PromptContext` and then does one of:
|
||
|
||
- `PromptSource::Dynamic`: call the Rust prompt builder directly
|
||
- `PromptSource::Inline` or `PromptSource::File`: load raw body, then wrap it with `render_subagent_system_prompt(...)`
|
||
|
||
Definition flags control which standard sections are omitted:
|
||
|
||
- `omit_identity`
|
||
- `omit_memory_context`
|
||
- `omit_safety_preamble`
|
||
- `omit_skills_catalog`
|
||
- `omit_profile`
|
||
- `omit_memory_md`
|
||
|
||
This is one of the main token-saving levers in the harness.
|
||
|
||
## The Subagent Inner Loop
|
||
|
||
The actual delegated execution happens in `run_inner_loop(...)`.
|
||
|
||
It is a slimmed-down tool loop:
|
||
|
||
- call provider
|
||
- parse tool calls
|
||
- persist transcript after provider response
|
||
- execute tools
|
||
- append results
|
||
- persist transcript again
|
||
- stop on final text or max iterations
|
||
|
||
It returns:
|
||
|
||
- final output text
|
||
- iteration count
|
||
- aggregated usage
|
||
|
||
Unlike the parent `Agent::turn`, it does not own the broader session lifecycle.
|
||
|
||
## Integrations Agent Special Cases
|
||
|
||
`integrations_agent` is the trickiest subagent path.
|
||
|
||
### Toolkit gate in `spawn_subagent`
|
||
|
||
If `agent_id == "integrations_agent"`:
|
||
|
||
- `toolkit` is mandatory
|
||
- the toolkit must exist in the allowlist
|
||
- if it exists but is not connected, the tool returns a success message explaining that authorization is required
|
||
|
||
This is intentionally not always treated as a hard tool failure, because disconnected integrations are a user-facing state, not necessarily a runtime error.
|
||
|
||
### Text-mode override
|
||
|
||
In `run_inner_loop`, `integrations_agent` with tool specs forces text mode instead of native tool calling.
|
||
|
||
Why:
|
||
|
||
- large Composio JSON schemas can blow provider grammar/context limits
|
||
|
||
What changes:
|
||
|
||
- tool specs are omitted from the API payload
|
||
- XML-style tool instructions are injected into the system prompt
|
||
- the runner parses `<tool_call>...</tool_call>` blocks out of plain text
|
||
- tool results in text mode are fed back as a user message containing `<tool_result>` tags
|
||
|
||
If a delegated integration run looks different from native-tool runs, this is usually why.
|
||
|
||
### Large result handoff cache
|
||
|
||
For toolkit-scoped `integrations_agent` runs, oversized tool results may be replaced by placeholders and stashed in an in-memory `ResultHandoffCache`.
|
||
|
||
The child can then call `extract_from_result(result_id, query)` to ask targeted follow-up questions against the cached payload.
|
||
|
||
This is not the same as generic payload summarization. It is a progressive-disclosure path specific to oversized delegated tool outputs.
|
||
|
||
## Parent -> Subagent -> Parent Result Shape
|
||
|
||
Conceptually the data flow is:
|
||
|
||
1. parent model emits `spawn_subagent(...)`
|
||
2. tool runtime executes the delegated subagent loop
|
||
3. subagent finishes with one final text output
|
||
4. `spawn_subagent` returns that text as its tool result
|
||
5. parent history receives the tool result
|
||
6. parent model gets another iteration and synthesizes the user-facing answer
|
||
|
||
The parent does not absorb the child’s internal reasoning trace or full message history. Only the compact final output crosses the boundary.
|
||
|
||
## Bus Path vs Session Path
|
||
|
||
There are two outer entrypoints to keep straight.
|
||
|
||
### `Agent::turn`
|
||
|
||
Used for full stateful sessions. This is the richer harness.
|
||
|
||
### `agent.run_turn` via `src/openhuman/agent/bus.rs`
|
||
|
||
This native event-bus handler calls `run_tool_call_loop(...)` directly using owned Rust payloads.
|
||
|
||
It supports:
|
||
|
||
- provider reuse
|
||
- tool filtering
|
||
- per-turn extra tools
|
||
- progress streaming
|
||
|
||
But it does not create a full `Agent` session object. If you are debugging channel-dispatch behavior, this distinction matters.
|
||
|
||
## Debugging Checklist
|
||
|
||
### 1. Confirm which execution tier you are in
|
||
|
||
Ask first:
|
||
|
||
- full `Agent::turn` session?
|
||
- bus `agent.run_turn` path?
|
||
- explicit `spawn_subagent` tool?
|
||
- synthesised `delegate_*` tool leading into `spawn_subagent`?
|
||
|
||
If you confuse these, logs will look contradictory.
|
||
|
||
### 2. Check registry state
|
||
|
||
If delegation fails very early, confirm:
|
||
|
||
- `AgentDefinitionRegistry::init_global(...)` ran at startup
|
||
- the target agent id exists
|
||
- workspace overrides did not shadow the expected built-in definition
|
||
|
||
### 3. Check task-local availability
|
||
|
||
If `run_subagent` errors with missing context:
|
||
|
||
- `NoParentContext` means the tool ran outside a parent turn
|
||
- `NoForkContext` means fork mode was requested but the fork snapshot was never installed
|
||
|
||
These are wiring issues, not prompt issues.
|
||
|
||
### 4. Check tool visibility vs tool execution
|
||
|
||
A tool can exist in the parent registry but still be invisible to a child due to:
|
||
|
||
- named `ToolScope`
|
||
- `disallowed_tools`
|
||
- `skill_filter`
|
||
- welcome-only stripping
|
||
- toolkit narrowing
|
||
|
||
If the model says “Unknown tool” or “not available to this sub-agent”, inspect filtering first.
|
||
|
||
### 5. Check transcript artifacts
|
||
|
||
Subagents persist transcripts per iteration using the parent session lineage plus a child session key. This is useful for debugging partial runs and crashes during tool execution.
|
||
|
||
Parent sessions and subagents do not write identical transcript shapes, so compare like with like.
|
||
|
||
### 6. Check the provider mode
|
||
|
||
If tool calling is malformed, verify whether the run used:
|
||
|
||
- native tools
|
||
- p-format / xml instructions
|
||
- integrations-agent text mode
|
||
|
||
The parser and message shape differ.
|
||
|
||
## Useful Log Prefixes
|
||
|
||
These prefixes are the most useful grep anchors:
|
||
|
||
- `[agent_loop]`
|
||
- `[agent]`
|
||
- `[tool-loop]`
|
||
- `[spawn_subagent]`
|
||
- `[subagent_runner]`
|
||
- `[subagent_runner:typed]`
|
||
- `[subagent_runner:fork]`
|
||
- `[subagent_runner:text-mode]`
|
||
- `[subagent_runner:handoff]`
|
||
- `[orchestrator_tools]`
|
||
- `[agent::bus]`
|
||
- `[transcript]`
|
||
|
||
## Best Existing Tests to Read First
|
||
|
||
For end-to-end harness behavior:
|
||
|
||
- `src/openhuman/agent/harness/session/tests.rs`
|
||
- `turn_dispatches_spawn_subagent_through_full_path`
|
||
- `turn_dispatches_spawn_subagent_in_fork_mode`
|
||
|
||
For runner behavior in isolation:
|
||
|
||
- `src/openhuman/agent/harness/subagent_runner.rs` tests
|
||
- typed mode returns text
|
||
- memory-context inclusion/omission
|
||
- tool filtering
|
||
- one-tool execution
|
||
- blocked tool recovery
|
||
- fork prefix replay
|
||
- missing parent/fork context errors
|
||
|
||
For orchestration-tool synthesis:
|
||
|
||
- `src/openhuman/tools/orchestrator_tools.rs` tests
|
||
|
||
For generic parent loop behavior:
|
||
|
||
- `src/openhuman/agent/tests.rs`
|
||
|
||
## Common Failure Modes
|
||
|
||
### Subagent never starts
|
||
|
||
Usually one of:
|
||
|
||
- registry not initialized
|
||
- invalid `agent_id`
|
||
- missing parent context
|
||
- missing fork context
|
||
|
||
### Subagent starts but cannot call expected tools
|
||
|
||
Usually one of:
|
||
|
||
- tool filtered out by definition scope
|
||
- `skill_filter` or toolkit override narrowed too aggressively
|
||
- tool is `CliRpcOnly`
|
||
- dynamic integration tools were not injected because the toolkit/client state was missing
|
||
|
||
### Integrations agent behaves unlike other agents
|
||
|
||
Usually expected. It may be in text mode and may be using the oversized-result handoff cache.
|
||
|
||
### Parent seems to “lose” child reasoning
|
||
|
||
Expected. Only the child’s final output is returned to the parent. Internal child history stays isolated.
|
||
|
||
## Practical Mental Model
|
||
|
||
The safest mental model is:
|
||
|
||
- the parent session is the durable conversation runtime
|
||
- tools are the execution boundary
|
||
- subagents are tool implementations that happen to run their own mini LLM loop
|
||
- fork mode is a cache-optimization path, not a different product feature
|
||
- `integrations_agent` is a special delegated runtime with extra provider and payload safeguards
|
||
|
||
If you debug from that model, the current codebase makes much more sense.
|