Repository navigation
fix(agent-bus): let a host's loop hooks reach sub-agents - #18
Merged
Merged
Conversation
AgentBus called run_agent_loop with neither runtime_hooks nor parser, so a
host that wraps the engine got its hooks on the agents it launches itself and
silently not on the sub-agents the bus spawns. The asymmetry has no runtime
symptom — the loop still runs, just with all-default no-op hooks — but it
costs per-tool timeout floors, the overflow spill-file recovery path,
sub-agent execution scopes (so cost and traces land on the parent's role),
session affinity, and tool-call parser policy.
Add configure_default_runtime_hooks / configure_default_tool_call_parser
alongside the existing pause-check and event-sink seams, and pass both through
at each dispatch site. Deliberately resolvers over the engine's own seams
rather than a replacement run_agent_loop: that module-global name is what
unit tests monkeypatch to keep a real loop out of a test, and shadowing it
would turn those into real loop runs. Resolvers rather than values so a host
below the loop package in its own layer stack can import lazily.
Also restore _schema_for_type's pre-port fall-through for `Any`: the
extraction added `if t is Any: return {}`, and an empty sub-schema reads as
"anything goes" but strict function-schema validators — Gemini-compatible
gateways among them — reject a property with no type, so a tool declared
`list[Any]` emitted `items: {}` and could 400 a whole fallback leg on schema
validation alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Summary
Found reviewing ApodexAI/MiroHarness#513, which replaces the product's own
agent_bus/bus.pywith a facade onto this one.AgentBuscalledrun_agent_loopwith neitherruntime_hooksnorparser. A host almost always wraps the engine with its own hooks, so the bus handed those to the agents the host launches itself and silently withheld them from every sub-agent it spawns. The asymmetry has no runtime symptom — the loop still runs, just with all-default no-op hooks.What that costs the host that found it: per-tool timeout floors, the overflow spill-file recovery path (both the
transform_resultwriter andbody_has_spill_reference, so "go read the spill file" could never fire inside a sub-agent), sub-agentExecutionScopeentry (cost and traces attributed to the parent's role instead of the sub-agent's), session affinity, and the parser policy that answers a hallucinated tool name with the list of available tools.Design note: seams, not a runner swap
The first attempt was a single
configure_default_loop_runnerthat replacedrun_agent_loopoutright. That shadows the module-global name unit tests monkeypatch to keep a real loop out of a test — nine test modules in MiroHarness alone — so those tests would have run the real loop and hung rather than failed. Verified: the product suite wedged at 34%.So this uses the engine's own
runtime_hooks/parserseams instead. The bus keeps callingrun_agent_loopby name; a host supplies resolvers for the two things it actually owns. Resolvers rather than values so a host sitting below the loop package in its own layer stack can import lazily (MiroHarness enforces that with a kernel-purity test).test_a_monkeypatched_engine_still_keeps_a_loop_out_of_a_unit_testpins that property.Also:
Anyparameters lost their schematypeThe extraction commit added
if t is Any: return {}to_schema_for_type, changing the pre-port fall-through to{"type": "string"}. An empty sub-schema reads as "anything goes", but strict function-schema validators — Gemini-compatible gateways among them — reject a property with notype, so a tool declaredlist[Any]started emittingitems: {}and could 400 a whole fallback leg on schema validation alone. Restored, with a comment pointing a tool that means "object" atdict[str, Any].Tests
tests/test_agent_bus_host_loop_seams.py(6) — both seams reach both dispatch paths, including the deferred session drain; resolution is at call time; the patchable name is not shadowed.tests/test_tool_schema_types.py(6) — no emitted property, at any nesting depth, lacks atype.Validation
ruff check agent_core testsclean;pytest -q921 passed.fix/session-spawn-yield) — different hunks ofbus.py, no semantic overlap. Verified on a throwaway merge: AgentCore 923 passed, MiroHarness 7148 passed.