feat(subagents): add invoke_agents batch tool for deterministic parallel fan-out#481
Open
rogeriomoura wants to merge 1 commit into
Open
Conversation
…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.
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
Adds a single
invoke_agentstool that fans a list of sub-agent tasks out internally viaasyncio.gather, instead of relying on the model/provider to honourparallel_tool_callsacross N separateinvoke_agentcalls.The headline win is who owns the concurrency. With N×
invoke_agentyou're at the provider's mercy — some serialize the calls, some reorder them, some ignoreparallel_tool_callsentirely. Withinvoke_agents, Code Puppy owns the fan-out, so parallelism, ordering, and failure handling are all deterministic.Why
invoke_agentsbeats N×invoke_agentinvoke_agents([...])invoke_agentin one turnasyncio.gather. Guaranteed parallel.parallel_tool_calls; some serialize anyway.zip(tasks, results))return_exceptions=True+ normalization → one bad sub-agent becomes a structurederrorfield, siblings unaffectedIn short:
invoke_agentsis the deterministic, reviewable, failure-isolated way to spawn a pack of sub-agents. N×invoke_agentis "fan out and hope the provider cooperates."What's in this PR
Core feature
tools/subagent_invocation.py: newregister_invoke_agentstool +SubAgentTaskmodel. Fans out withasyncio.gather(..., return_exceptions=True)and normalizes any escaped exception into a structuredAgentInvokeOutput(error=...)so one failure never poisons the batch.SubAgentTaskcarries an optional per-taskmodel_nameoverride (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. ABeforeValidatortolerates providers that double-encode thetasksargument as a JSON string.Parallel-safe session context
messaging/bus.py: session context moves from a lock-guarded shared attribute into a module-levelContextVarwithset/resettoken semantics. Becauseasynciocopies 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. Newreset_session_contextis exported alongside the existing helpers.Opt-in parallelism outside yolo mode
config.get_allow_parallel_tool_calls(): new flag (defaultFalse). When off, parallel tool calls stay disabled outside yolo mode so the user can review each call sequentially.model_factory.make_model_settingshonours it. (Note:invoke_agentsitself 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 compactN OK / M FAILper-agent summary instead of a?.Wiring
invoke_agentsinTOOL_REGISTRYand the code-puppy agent's available tools.Tests
model_nameoverride reaches_invoke_agent_impl(custom / blank→None / omitted→None).resolved_model_name().129 passed.ruff checkandruff formatclean.Backwards compatibility
Purely additive. Existing
invoke_agent/invoke_agent_with_modelare untouched, the newmodel_namefield is optional, and theallow_parallel_tool_callsflag defaults to the prior behaviour.