feat: agent delegation mechanism (fixes #63)#123
Conversation
- Introduced `delegate_to_agent` tool for orchestrating agent tasks. - Enhanced `AgentPool` to manage available run slots, preventing deadlocks during nested runs. - Updated `TeamInfo` and `RunOptions` to support delegation context. - Added tests for delegation functionality, including error handling for self-delegation and depth limits. - Refactored built-in tools registration to conditionally include the new delegation tool.
Adds optional `metadata.tokenUsage` to `ToolResult` so tools that perform nested LLM work can report their consumption. `delegate_to_agent` now sets it from the delegated `AgentRunResult.tokenUsage`, and the runner rolls each turn's delegation usage into its `totalUsage` before the next `maxTokenBudget` check. Closes the non-blocking review item from PR #84: without this, a parent agent could issue delegations that silently bypass its own budget cap.
Adds `TeamInfo.delegationChain` (readonly string[]) tracking the chain of agents from the root task to the current one. The orchestrator seeds it with the top-level assignee and appends each target on recursive `runDelegatedAgent` calls. `delegate_to_agent` checks whether the target is already in the chain before the depth check and rejects with the full chain in the error message. Closes the non-blocking review item from PR #84: without this, an A -> B -> A cycle would burn up to `maxDelegationDepth` LLM calls before aborting. It now aborts on the first cycle attempt.
Both `compressConsumedToolResults` (turn-based marker replacement) and the `compact` context strategy now skip tool_result blocks whose originating tool_use was `delegate_to_agent`. Delegation output often carries the full sub-agent answer the parent needs to reason over in later turns; compressing it leaves the parent with a meaningless placeholder. Error results and short results are still handled as before.
Documents delegate_to_agent: opt-in registration, guard order (self/unknown/cycle/depth/pool), parent-run tokenUsage aggregation, compression/compact exemption, and SharedMemory audit. Notes that standalone runAgent and the runTeam short-circuit path do not inject the tool. Also lists glob in the built-in tool set.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eb983ea630
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (team.delegationPool !== undefined && team.delegationPool.availableRunSlots < 1) { | ||
| return { | ||
| data: | ||
| 'Agent pool has no free concurrency slot for a delegated run (nested run would block indefinitely). ' + | ||
| 'Increase orchestrator maxConcurrency, wait for parallel work to finish, or avoid delegating while the pool is saturated.', |
There was a problem hiding this comment.
Block delegation to agents already executing a run
This deadlock guard only checks free pool slots, but pool.run also waits on a per-agent lock. In a parallel run, if agent A delegates to B while B simultaneously delegates to A, each caller still holds its own lock and each delegated call waits on the other lock forever. This can happen even when availableRunSlots > 0 (for example with maxConcurrency 3 and two active parent runs), so the current check does not prevent a hard hang.
Useful? React with 👍 / 👎.
| if (delegationTurnUsage !== undefined && this.options.maxTokenBudget !== undefined) { | ||
| const totalAfterDelegation = totalUsage.input_tokens + totalUsage.output_tokens | ||
| if (totalAfterDelegation > this.options.maxTokenBudget) { | ||
| budgetExceeded = true |
There was a problem hiding this comment.
Record tool outputs before budget-exceeded early exit
When delegation token usage pushes the run over maxTokenBudget, this branch exits the loop before Step 5 appends tool_result messages and allToolCalls. That means delegated tools may have already executed but their outputs/call records are missing from the returned run result, which creates inconsistent observability and can hide executed side effects from callers.
Useful? React with 👍 / 👎.
…token budget When nested-run tokenUsage pushed totalUsage past maxTokenBudget, the runner broke out of the loop before yielding tool_result events and before appending the tool_result user message. Stream consumers saw dangling tool_use events, and the returned `messages` ended with an assistant tool_use block but no matching tool_result, making the conversation unresumable against the underlying LLM APIs. Defer the delegation budget check until after the tool_result events have been yielded and the user message has been pushed, so: - matched tool_use/tool_result pairs always reach stream consumers, - allToolCalls records every executed tool, and - returned messages remain API-resumable. Addresses Codex P2 on PR #123.
…tion deadlock
AgentPool.run acquires a per-agent lock meant to serialize concurrent
mutations of a shared Agent instance. When two parent tasks mutually
delegate (A→B while B→A), each nested pool.run waited on the other
agent's lock forever — even with free pool slots — because both parents
held their own lock until their tool call returned.
Delegation already promises "a fresh conversation for this prompt only",
so reusing the long-lived pool Agent was the wrong target. Each
delegation now:
- looks up the target AgentConfig from the team roster,
- applies orchestrator defaults (provider / baseURL / apiKey),
- builds a temporary Agent via buildAgent(..., { includeDelegateTool: true }),
- runs it via the new AgentPool.runEphemeral, which acquires only the
pool semaphore.
The lock-based deadlock becomes structurally impossible. Parallel
delegations to the same target now each get their own temp instance
instead of queueing on a shared lock.
Addresses Codex P1 on PR #123.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Summary
Implements the
delegate_to_agentbuilt-in tool from #63 — an orchestrated agent can synchronously hand a sub-prompt to another roster agent and receive that agent's final output as a normal tool result.Picks up the work from #84 (closed for inactivity) and lands the three non-blocking items flagged in that PR's review.
What's in this PR
delegate_to_agenttool with opt-in registration viaincludeDelegateTool(only wired byrunTeam/runTasks, not standalonerunAgent)AgentPool.availableRunSlotsmaxDelegationDepth, default 3)TeamInfo.delegationChain—A -> B -> Aaborts on the first cycle attempt instead of burning up to 3 turns of LLM callstokenUsagesurfaces viaToolResult.metadata.tokenUsageand accumulates into the parent runner'stotalUsagebefore the nextmaxTokenBudgetcheck, so delegation can't silently bypass the parent's budgetcompressToolResultsand thecompactcontext strategy so the parent retains the full sub-agent output across turns{caller}/delegation:{target}:{timestamp}-{rand}isSimpleGoal) path keeps the single-agent optimization and does not inject the delegation toolexamples/16-agent-handoff.ts, new section inCLAUDE.mdCredit
The base implementation (registration plumbing, depth/pool/self/unknown guards,
TeamInfoshape, audit writes, example, the bulk of the tests) is @NamelessNATM's design and code from #84. This PR rebases that work onto current main, resolves conflicts with PRs landed since (#70 #71 #72 #83 #110 #111 #115 #116 #117 #118 #121), and adds the three follow-up commits flagged in the original review.When merged, please use Squash and merge so the squash message keeps a
Co-authored-by:trailer for @NamelessNATM.Test plan
npm run lintpassesnpm testpasses (622 tests)examples/16-agent-handoff.tsruns end-to-end with a real API key