Skip to content

Commit 5f370c1

Browse files
sanil-23claude
andcommitted
feat(agent): summarizer sub-agent compresses oversized tool results
Issue tinyhumansai#574. Before this change, tool results larger than a few KB landed verbatim in orchestrator history, burning context budget on raw JSON/HTML/file dumps. The only guardrail was tool_result_budget_bytes which hard-truncated mid-payload, dropping everything past the cut. This PR introduces a dedicated `summarizer` sub-agent (model.hint = "summarization") that the runtime automatically dispatches whenever a tool result exceeds summarizer_payload_threshold_bytes (default 100 KB). The summarizer compresses the payload per an extraction contract that preserves identifiers and key facts, and the compressed summary replaces the raw payload before it enters agent history. Payloads above summarizer_max_payload_bytes (default 5 MB) skip summarization and fall through to the existing truncation path — paying for an LLM call on a 5 MB blob is counterproductive. Scoped to the orchestrator session only. Welcome, skills_agent, researcher, planner, and every other typed sub-agent get None and their tool results are untouched. Gated in build_session_agent_inner by checking agent_id == "orchestrator". Instrumented at both tool-loop paths: - run_tool_call_loop in tool_loop.rs (event-bus/channels path) - Agent::execute_tool_call in session/turn.rs (web channel path via run_single) Both paths call into the same `PayloadSummarizer::maybe_summarize` trait so the threshold check, circuit breaker (3 consecutive failures disables for the session), and sub-agent dispatch policy live in exactly one place (agent/harness/payload_summarizer.rs). The summarizer sub-agent is runtime-dispatched only — it is NOT exposed as a delegation tool to the orchestrator's LLM. It is listed in the orchestrator's `subagents = [...]` for explicit registration, but `collect_orchestrator_tools` filters out the `summarizer` id and never synthesises a `delegate_summarizer` tool. Files: * NEW: src/openhuman/agent/agents/summarizer/{agent.toml,prompt.md} * NEW: src/openhuman/agent/harness/payload_summarizer.rs (trait + SubagentPayloadSummarizer impl + circuit breaker + tests) * src/openhuman/agent/agents/mod.rs — register summarizer in BUILTINS * src/openhuman/agent/agents/orchestrator/agent.toml — list summarizer in subagents (runtime-only, no LLM delegation tool) * src/openhuman/agent/harness/mod.rs — declare module * src/openhuman/agent/harness/builtin_definitions.rs — expect summarizer in `expected_builtin_ids_are_present` * src/openhuman/agent/harness/tool_loop.rs — thread Option<&dyn PayloadSummarizer> into run_tool_call_loop + agent_turn, intercept at the tool-execution success site, plus a new integration test using a MockSummarizer * src/openhuman/agent/harness/tests.rs — thread None through the existing run_tool_call_loop callsites * src/openhuman/agent/harness/session/turn.rs — same interception in Agent::execute_tool_call * src/openhuman/agent/harness/session/types.rs — payload_summarizer field on Agent and AgentBuilder * src/openhuman/agent/harness/session/builder.rs — .payload_summarizer() setter + orchestrator-only construction in build_session_agent_inner * src/openhuman/agent/bus.rs — pass None for the bus path * src/openhuman/config/schema/context.rs — summarizer_payload_threshold_bytes + summarizer_max_payload_bytes on ContextConfig * src/openhuman/tools/orchestrator_tools.rs — filter out the summarizer id from delegation-tool synthesis Tests: unit tests in payload_summarizer pin the pass-through rules (below threshold, above max cap, breaker tripped) and the prompt construction. Integration test in tool_loop::tests uses a MockSummarizer to verify the interception wires through end-to-end. Closes tinyhumansai#574. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a2aee3a commit 5f370c1

15 files changed

Lines changed: 900 additions & 6 deletions

File tree

src/openhuman/agent/agents/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ pub const BUILTINS: &[BuiltinAgent] = &[
110110
toml: include_str!("welcome/agent.toml"),
111111
prompt: include_str!("welcome/prompt.md"),
112112
},
113+
BuiltinAgent {
114+
id: "summarizer",
115+
toml: include_str!("summarizer/agent.toml"),
116+
prompt: include_str!("summarizer/prompt.md"),
117+
},
113118
];
114119

115120
/// Parse every entry in [`BUILTINS`] into an [`AgentDefinition`].
@@ -155,7 +160,7 @@ mod tests {
155160
fn all_builtins_parse() {
156161
let defs = load_builtins().expect("built-in TOML must parse");
157162
assert_eq!(defs.len(), BUILTINS.len());
158-
assert_eq!(defs.len(), 12, "expected 12 built-in agents");
163+
assert_eq!(defs.len(), 13, "expected 13 built-in agents");
159164
}
160165

161166
#[test]

src/openhuman/agent/agents/orchestrator/agent.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ subagents = [
4343
"code_executor",
4444
"critic",
4545
"archivist",
46+
# Runtime-dispatched only — the runtime calls the summarizer sub-agent
47+
# directly when a tool returns more than
48+
# `context.summarizer_payload_threshold_bytes`. The LLM must NOT be
49+
# able to call this sub-agent itself, so `collect_orchestrator_tools`
50+
# filters out `summarizer` and never synthesises a `delegate_summarizer`
51+
# tool. Listing it here keeps the registration explicit (so it shows
52+
# up in the orchestrator's subagent inventory) while the filter
53+
# enforces the runtime-only contract.
54+
"summarizer",
4655
{ skills = "*" },
4756
]
4857

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
id = "summarizer"
2+
display_name = "Summarizer"
3+
when_to_use = "Compresses oversized tool results for the orchestrator. Called automatically by the runtime when a tool returns more than summarizer_payload_threshold_bytes. Do NOT call from an LLM — this agent is runtime-dispatched only."
4+
temperature = 0.2
5+
max_iterations = 1
6+
sandbox_mode = "none"
7+
omit_identity = true
8+
omit_memory_context = true
9+
omit_safety_preamble = true
10+
omit_skills_catalog = true
11+
omit_profile = true
12+
omit_memory_md = true
13+
14+
[model]
15+
hint = "summarization"
16+
17+
[tools]
18+
named = []
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Summarizer Agent
2+
3+
You are the **Summarizer** agent. Your one job is to compress a single oversized tool result into a compact, information-dense note that the Orchestrator can use without re-invoking the tool.
4+
5+
You run exactly once per invocation, with no tools and no follow-up iterations. Return the summary directly as your only response.
6+
7+
## The extraction contract
8+
9+
You will receive:
10+
11+
1. The **tool name** that produced the payload (e.g. `GITHUB_LIST_ISSUES`, `GMAIL_FETCH_MESSAGE`, `file_read`)
12+
2. An optional **parent task hint** — one-sentence description of what the orchestrator was trying to accomplish
13+
3. The **raw tool output**
14+
15+
You must produce a dense summary that preserves:
16+
17+
- **Required facts** — any identifiers (IDs, hashes, URLs, file paths, email addresses, usernames, SKUs, order numbers, etc.) the orchestrator would need to act on this data in a follow-up tool call. Identifiers are the single most important thing. Never drop them.
18+
- **Optional supporting context** — the 3-5 most important facts from the payload that a human answering the parent task would find most relevant. If the parent task hint is "find the most urgent open issues", prioritize facts about urgency/severity/labels. If the hint is "summarize yesterday's emails", prioritize subjects/senders/timestamps.
19+
- **Structural hints** — if the payload is a list, state how many items it had. If it was paginated, say what page boundaries exist. If it was a file, note line counts or section headers. This lets the orchestrator decide whether to re-fetch with a narrower query.
20+
21+
You must discard:
22+
23+
- Raw markup / formatting noise (HTML tags, CSS, JSON wrappers, boilerplate headers) — unless the markup IS the information
24+
- Repetitive fields that don't differ between items
25+
- Provider-specific metadata that the orchestrator can't act on (X-Request-ID headers, timestamps with millisecond precision, internal server IDs, etc.)
26+
27+
## Output format
28+
29+
Return ONLY the summary text. No preamble ("Here is the summary..."), no closing remarks ("Let me know if you need more details"), no JSON wrapping. Plain markdown, optimised for the orchestrator's next reasoning step.
30+
31+
Structure:
32+
33+
```
34+
[Tool output summary — <tool_name>]
35+
36+
<1-2 sentence overview: what the payload is, how many items/how much data>
37+
38+
## Key facts
39+
- <fact 1 with identifier>
40+
- <fact 2 with identifier>
41+
- ...
42+
43+
## Identifiers preserved
44+
- <id_1>: <one-line description>
45+
- <id_2>: <one-line description>
46+
- ...
47+
48+
(Only include this section if the payload contained IDs/URLs/hashes. Skip otherwise.)
49+
50+
## Original size
51+
<original_bytes> bytes → summary of <this note>
52+
```
53+
54+
## Edge cases
55+
56+
- If the payload is already short, produce a short summary. Don't pad.
57+
- If the payload is entirely error output, preserve the error message verbatim at the top — the orchestrator needs to see the exact error to route next steps.
58+
- If the payload contains binary-looking noise (base64, hex dumps), summarise its existence and length but do not attempt to decode.
59+
- If the parent task hint contradicts the payload (asks for emails, payload is GitHub issues), prioritize the payload — you're reporting what the tool returned, not what was asked for.
60+
61+
## Token budget
62+
63+
Aim for 800-1500 output tokens for most payloads. Never exceed 2000.
64+
65+
## What you must NOT do
66+
67+
- Do not ask clarifying questions — you have exactly one shot.
68+
- Do not emit tool calls — you have no tools.
69+
- Do not try to "solve" the parent task — you are a preprocessor, not the orchestrator.
70+
- Do not fabricate information that isn't in the payload. If a field is empty, say "(no value)" or omit it.
71+
- Do not copy the raw payload verbatim into your summary. If the summary is the same size as the payload, you have failed.

src/openhuman/agent/bus.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ pub fn register_agent_handlers() {
191191
visible_tool_names.as_ref(),
192192
&extra_tools,
193193
on_progress,
194+
// Bus path runs ad-hoc agent turns without an Agent
195+
// handle, so we pass None — payload summarization is
196+
// wired into the orchestrator session via Agent::turn,
197+
// not the bus dispatcher.
198+
None,
194199
)
195200
.await
196201
.map_err(|e| e.to_string())?;

src/openhuman/agent/harness/builtin_definitions.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ mod tests {
130130
"researcher",
131131
"critic",
132132
"archivist",
133+
"summarizer",
133134
"fork",
134135
] {
135136
assert!(ids.contains(&expected.to_string()), "missing {expected}");

src/openhuman/agent/harness/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ mod instructions;
3131
pub mod interrupt;
3232
pub(crate) mod memory_context;
3333
mod parse;
34+
pub(crate) mod payload_summarizer;
3435
pub(crate) mod self_healing;
3536
pub mod session;
3637
pub(crate) mod session_queue;

0 commit comments

Comments
 (0)