feat(harness): add the progressive-disclosure handoff cache - #103
Conversation
Ported from OpenHuman's `agent/harness/subagent_runner/handoff.rs`. It is the movable part of Phase 5's `subagent_runner` family; the runner core itself is not movable yet (see the PR body). The problem it solves: a sub-agent calls a tool that returns a megabyte-scale payload, that blob goes into history as a tool-result message, and the NEXT iteration ships the bloated history back to the provider where it hits the context ceiling. One oversized result poisons every turn after it. Progressive disclosure stashes the full payload, substitutes a short placeholder carrying size + preview + a result id + how to query it, and lets the agent decide whether the preview already answers its task. The extraction step only runs when it asks for a narrower view. This is the sibling strategy to `harness::artifacts`: same problem, two different answers. Artifacts writes a deliverable to disk and hands on a path; handoff keeps an in-memory, FIFO-evicting, per-spawn cache for payloads nobody wants to persist. Two changes from the original, both deliberate: - `apply_handoff` takes `threshold_tokens` instead of reading the threshold from an `OPENHUMAN_TEST_HANDOFF_THRESHOLD_TOKENS` environment variable. A host-named env backdoor has no place in a redistributed crate, a host legitimately varies the threshold per agent or per model context window, and env-var-driven tests race under parallel execution. - The extraction tool stays host-side. A tool is host vocabulary, and OpenHuman's reaches its transcript, `TurnModelSource` and inference routing. This module owns the cache and the placeholder, not the query. The module had NO tests where it came from - it was covered only indirectly through the sub-agent runner's integration tests. 16 are added here, aimed at the failures that would be silent rather than loud: - eviction is FIFO and bounded, so it drops the oldest rather than the entry a model is most likely to ask about - the placeholder's advertised result id actually resolves, or the model is told to call a tool that will find nothing - an `Error` result passes through however large, because errors are diagnostic text the agent must read directly rather than a payload to hide behind another call - an extraction result is never re-stashed, which would otherwise loop: stash the answer, hand back a placeholder, extract again - the preview is taken by chars, not bytes, so a multibyte payload cannot panic the renderer Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add the `regex` crate as a direct dependency to support stripping markup, data-URIs, and whitespace from oversized tool payloads in the harness handoff module. This dependency has already been resolved in the OpenHuman kernel profile, so it introduces no new packages there. Auto-committed-on: macbook Co-authored-by: Medulla <medulla@tinyhumans.ai>
|
Warning Review limit reached
Next review available in: 52 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Ported from OpenHuman's
agent/harness/subagent_runner/handoff.rs. This is the movable part of Phase 5'ssubagent_runnerfamily — see the scope note at the bottom for why the rest is not.Independent of #102, with one overlap: both add
regex = "1"toCargo.toml. Whichever merges second drops that hunk; either order works.What it does
A sub-agent calls a tool that returns a megabyte-scale payload (
GMAIL_LIST_MESSAGES,NOTION_GET_PAGE, a bulk Drive listing). That blob goes into history as a tool-result message, and the next iteration ships the bloated history back to the provider, where it hits the context ceiling. One oversized result poisons every turn after it.Progressive disclosure stashes the full payload, substitutes a short placeholder carrying size + preview + a
result_id+ how to query it, and lets the agent decide whether the preview already answers its task. The extraction step runs only when it asks for a narrower view.Sibling strategy to
harness::artifacts(#101): same problem, two different answers. Artifacts writes a deliverable to disk and hands on a path; handoff keeps an in-memory, FIFO-evicting, per-spawn cache for payloads nobody wants to persist.Two deliberate changes from the original
apply_handofftakesthreshold_tokensinstead of readingOPENHUMAN_TEST_HANDOFF_THRESHOLD_TOKENS. A host-named environment backdoor has no place in a redistributed crate; a host legitimately varies the threshold per agent or per model context window; and env-var-driven tests race under parallel execution. Callers passHANDOFF_OVERSIZE_THRESHOLD_TOKENSfor the previous default.TurnModelSource, and inference routing. This module owns the cache and the placeholder, never the query — the same line drawn in feat(harness): add a generic artifact-offload module #101 and feat(harness): add tool-call parsing as harness::tool_calling #102.Tests
The module had none where it came from — it was covered only indirectly, through the sub-agent runner's integration tests. 16 are added, aimed at failures that would be silent rather than loud:
eviction_is_fifo_and_boundedan_oversized_result_is_stashed_and_replaced_by_a_placeholderresult_idnot resolving, so the model is told to call a tool that finds nothingan_error_result_passes_through_however_largean_extraction_result_is_never_re_stashedthe_preview_never_splits_a_multibyte_characterthe_threshold_is_honoured_in_both_directionsids_are_unique_across_storescargo test --all-features --lib— 1782 passed, 0 failedcargo clippy --all-features --all-targets— cleancargo fmt --check— cleanScope note: why the rest of
subagent_runneris not hereMeasured rather than assumed. The family is 7,471 LOC with 203 references across 10 OpenHuman domains:
ops/runner.rsops/graph.rsops/provider.rstool_prep.rsextract_tool.rshandoff.rsThe centre of gravity — runner + graph + provider, 3,516 LOC and 111 references — reaches Composio, config, memory, inference, security and the desktop surface. That is exactly the coupling the capability traits exist to invert, so moving it before the host repointing lands would mean carrying those domains into the crate rather than injecting them.
handoff.rsis the one file with genuinely zero host coupling, so it is the one that moves now.extract_tool.rsis its natural partner and still stays: it is a hostToolimplementation reaching the transcript,TurnModelSource, andinference::context_window_for_model.🤖 Generated with Claude Code