Skip to content

feat(subagents): add invoke_agents batch tool for deterministic parallel fan-out#481

Open
rogeriomoura wants to merge 1 commit into
mpfaffenberger:mainfrom
rogeriomoura:feat/invoke-agents-parallel-fanout
Open

feat(subagents): add invoke_agents batch tool for deterministic parallel fan-out#481
rogeriomoura wants to merge 1 commit into
mpfaffenberger:mainfrom
rogeriomoura:feat/invoke-agents-parallel-fanout

Conversation

@rogeriomoura

Copy link
Copy Markdown
Contributor

Summary

Adds a single invoke_agents tool that fans a list of sub-agent tasks out internally via asyncio.gather, instead of relying on the model/provider to honour parallel_tool_calls across N separate invoke_agent calls.

The headline win is who owns the concurrency. With N×invoke_agent you're at the provider's mercy — some serialize the calls, some reorder them, some ignore parallel_tool_calls entirely. With invoke_agents, Code Puppy owns the fan-out, so parallelism, ordering, and failure handling are all deterministic.

invoke_agents(tasks=[
    {"agent_name": "code-puppy", "prompt": "review file A"},
    {"agent_name": "code-puppy", "prompt": "review file B"},
    {"agent_name": "qa-kitten",  "prompt": "smoke-test the app"},
])

Why invoke_agents beats N×invoke_agent

Dimension invoke_agents([...]) invoke_agent in one turn
Who owns parallelism Code Puppy — one tool call fans out via asyncio.gather. Guaranteed parallel. The provider. Depends on the model honouring parallel_tool_calls; some serialize anyway.
Tool calls emitted 1 reviewable call N scattered calls
Result ordering Guaranteed order-preserved (zip(tasks, results)) Depends on how the harness stitches responses back
Failure isolation Built-in: return_exceptions=True + normalization → one bad sub-agent becomes a structured error field, siblings unaffected No unified "collect everything" guarantee
Session isolation Each task gets its own asyncio Task → ContextVar session isolation holds Trusting the provider not to interleave weirdly
Auditability / DRY One intent, one diff to review Repetition of the same tool shape N times

In short: invoke_agents is the deterministic, reviewable, failure-isolated way to spawn a pack of sub-agents. N×invoke_agent is "fan out and hope the provider cooperates."

What's in this PR

Core feature

  • tools/subagent_invocation.py: new register_invoke_agents tool + SubAgentTask model. Fans out with asyncio.gather(..., return_exceptions=True) and normalizes any escaped exception into a structured AgentInvokeOutput(error=...) so one failure never poisons the batch.
  • SubAgentTask carries an optional per-task model_name override (omit it for the agent's configured model). This closes the "parallel + custom models" gap — the batch path is no longer limited to each agent's default model. A BeforeValidator tolerates providers that double-encode the tasks argument as a JSON string.

Parallel-safe session context

  • messaging/bus.py: session context moves from a lock-guarded shared attribute into a module-level ContextVar with set/reset token semantics. Because asyncio copies the current context when a Task is created, every fanned-out sub-agent sees its own session id and can't clobber a sibling's. New reset_session_context is exported alongside the existing helpers.

Opt-in parallelism outside yolo mode

  • config.get_allow_parallel_tool_calls(): new flag (default False). When off, parallel tool calls stay disabled outside yolo mode so the user can review each call sequentially. model_factory.make_model_settings honours it. (Note: invoke_agents itself does not require this flag — it's a single reviewable call that fans out internally.)

Display

  • agents/run_stats.py: renders the batch (list) result as a compact N OK / M FAIL per-agent summary instead of a ?.

Wiring

  • Registered invoke_agents in TOOL_REGISTRY and the code-puppy agent's available tools.

Tests

  • Per-task model_name override reaches _invoke_agent_impl (custom / blank→None / omitted→None).
  • Blank-normalization contract for resolved_model_name().
  • ContextVar session isolation across concurrent sub-agents.
  • Batch failure isolation (one bad puppy, healthy siblings).

129 passed. ruff check and ruff format clean.

Backwards compatibility

Purely additive. Existing invoke_agent / invoke_agent_with_model are untouched, the new model_name field is optional, and the allow_parallel_tool_calls flag defaults to the prior behaviour.

…lel fan-out

Introduce a single `invoke_agents` tool that fans a list of sub-agent
tasks out internally via `asyncio.gather`, instead of relying on the
model/provider to honour `parallel_tool_calls` across N separate
`invoke_agent` calls.

Why this beats N x invoke_agent:
- Deterministic parallelism: Code Puppy owns the concurrency, not the
  provider. Guaranteed to actually run in parallel.
- One reviewable tool call instead of N scattered ones.
- Order-preserved results (zip(tasks, results)).
- Failure isolation: return_exceptions=True + normalization means one
  bad sub-agent becomes a structured `error` field, siblings unaffected.

Supporting changes:
- messaging/bus.py: move session context into a ContextVar with
  set/reset token semantics so concurrently-running sub-agents stay
  isolated and never clobber a sibling's session id. Export
  reset_session_context.
- SubAgentTask gains an optional per-task `model_name` override (omit
  for the agent's configured model), closing the "parallel + custom
  models" gap. A BeforeValidator tolerates providers that double-encode
  the `tasks` arg as a JSON string.
- config.get_allow_parallel_tool_calls(): opt-in flag to permit parallel
  tool calls outside yolo mode; model_factory honours it.
- run_stats.py: render the batch (list) result as an OK/FAIL summary.
- Register invoke_agents in the tool registry and the code-puppy agent.

Tests: per-task model override plumbing, blank-normalization contract,
ContextVar session isolation, and batch failure isolation. 129 passing.
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