Skip to content

fix(agent-bus): let a host's loop hooks reach sub-agents - #18

Merged
zhanghanduo merged 1 commit into
mainfrom
fix/agent-bus-loop-runner-hook
Sep 2, 2026
Merged

zhanghanduo merged 1 commit into
mainfrom
fix/agent-bus-loop-runner-hook

Conversation

@zhanghanduo

Copy link
Copy Markdown
Collaborator

Summary

Found reviewing ApodexAI/MiroHarness#513, which replaces the product's own agent_bus/bus.py with a facade onto this one.

AgentBus called run_agent_loop with neither runtime_hooks nor parser. 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_result writer and body_has_spill_reference, so "go read the spill file" could never fire inside a sub-agent), sub-agent ExecutionScope entry (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_runner that replaced run_agent_loop outright. 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 / parser seams instead. The bus keeps calling run_agent_loop by 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_test pins that property.

Also: Any parameters lost their schema type

The 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 no type, so a tool declared list[Any] started emitting items: {} and could 400 a whole fallback leg on schema validation alone. Restored, with a comment pointing a tool that means "object" at dict[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 a type.

Validation

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>
@zhanghanduo
zhanghanduo merged commit b5ee0c5 into main Sep 2, 2026
1 check passed
@zhanghanduo
zhanghanduo deleted the fix/agent-bus-loop-runner-hook branch September 2, 2026 07:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant