Skip to content

feat: agent delegation mechanism (fixes #63)#123

Merged
JackChen-me merged 7 commits into
mainfrom
feat/delegation-63
Apr 19, 2026
Merged

feat: agent delegation mechanism (fixes #63)#123
JackChen-me merged 7 commits into
mainfrom
feat/delegation-63

Conversation

@JackChen-me
Copy link
Copy Markdown
Member

Summary

Implements the delegate_to_agent built-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_agent tool with opt-in registration via includeDelegateTool (only wired by runTeam/runTasks, not standalone runAgent)
  • Pool deadlock guard via AgentPool.availableRunSlots
  • Self-delegation rejected; unknown target rejected; depth limit (maxDelegationDepth, default 3)
  • Cycle detection via TeamInfo.delegationChainA -> B -> A aborts on the first cycle attempt instead of burning up to 3 turns of LLM calls
  • Token aggregation — delegated run's tokenUsage surfaces via ToolResult.metadata.tokenUsage and accumulates into the parent runner's totalUsage before the next maxTokenBudget check, so delegation can't silently bypass the parent's budget
  • Compression exemption — delegation tool_result blocks survive both compressToolResults and the compact context strategy so the parent retains the full sub-agent output across turns
  • Best-effort SharedMemory audit writes at {caller}/delegation:{target}:{timestamp}-{rand}
  • Short-circuit (isSimpleGoal) path keeps the single-agent optimization and does not inject the delegation tool
  • New example examples/16-agent-handoff.ts, new section in CLAUDE.md

Credit

The base implementation (registration plumbing, depth/pool/self/unknown guards, TeamInfo shape, 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 lint passes
  • npm test passes (622 tests)
  • examples/16-agent-handoff.ts runs end-to-end with a real API key

NamelessNATM and others added 5 commits April 19, 2026 02:03
- 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.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +78 to +82
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.',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/agent/runner.ts Outdated
Comment on lines +823 to +826
if (delegationTurnUsage !== undefined && this.options.maxTokenBudget !== undefined) {
const totalAfterDelegation = totalUsage.input_tokens + totalUsage.output_tokens
if (totalAfterDelegation > this.options.maxTokenBudget) {
budgetExceeded = true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@JackChen-me JackChen-me merged commit b857c00 into main Apr 19, 2026
3 checks passed
@JackChen-me JackChen-me deleted the feat/delegation-63 branch April 19, 2026 02:54
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.

2 participants