Skip to content

[P1] Agent Handoff — delegate_to_agent built-in tool #58

@JackChen-me

Description

@JackChen-me

Context

Multi-agent frameworks increasingly support agent handoff as a first-class pattern — the ability for one agent to transfer control to another agent mid-conversation. OpenAI Swarm, Anthropic Agent SDK, and LangGraph all have handoff primitives. Today, agents in open-multi-agent collaborate via SharedMemory (write results for others to read) and MessageBus (point-to-point messages), but there is no way for an agent to delegate a sub-task to another agent and get the result back within a single conversation loop.

This means:

  • An agent can't say "I need the researcher to look this up" and get the answer in-context
  • Multi-step workflows that require dynamic routing (e.g. "if code review fails, hand off to the fixer agent") must be orchestrated externally
  • The coordinator pattern works for pre-planned DAGs, but not for emergent, runtime delegation

A delegate_to_agent tool fills this gap without changing the framework's architecture.

Proposed Implementation

New built-in tool: delegate_to_agent

Register a new tool in src/tool/built-in/ that allows any agent to invoke another agent in the same team:

const researcher: AgentConfig = {
  name: 'researcher',
  model: 'claude-sonnet-4-6',
  systemPrompt: 'You research topics thoroughly.',
  tools: ['file_read', 'grep'],
}

const writer: AgentConfig = {
  name: 'writer',
  model: 'claude-sonnet-4-6',
  systemPrompt: 'You write clear documentation.',
  tools: ['file_write', 'delegate_to_agent'],  // <-- can delegate
}

// During execution, the writer agent can call:
// delegate_to_agent({ agent: "researcher", task: "Find the latest benchmarks for Claude vs GPT" })
// → receives the researcher's output as a tool result

Tool schema

defineTool({
  name: 'delegate_to_agent',
  description: 'Delegate a sub-task to another agent on the team and get the result back.',
  inputSchema: z.object({
    agent: z.string().describe('Name of the agent to delegate to'),
    task: z.string().describe('Description of the sub-task for the delegate agent'),
  }),
  execute: async ({ agent, task }, context) => {
    // 1. Look up the target agent in context.team
    // 2. Run the target agent with `task` as prompt
    // 3. Return the agent's output as the tool result
    // 4. Write to SharedMemory for audit trail
  },
})

Integration points

  1. ToolUseContext (types.ts:126): Already has team?: TeamInfo — the delegation tool needs access to the team's agent pool. Extend TeamInfo or the context to include a reference to a run function (e.g. delegateFn).

  2. AgentRunner (agent/runner.ts): No changes needed — the tool executes like any other tool. The target agent run happens inside the tool's execute function.

  3. Built-in tools (tool/built-in/): Add delegate.ts alongside existing tools. Register it in registerBuiltInTools() conditionally (only when team context is available).

  4. SharedMemory: Delegation results are written to shared memory under the delegating agent's namespace (writer/delegation:researcher:result) for observability.

Key decisions

  • Tool-based, not protocol-based — handoff is just another tool call. The LLM decides when to delegate, no special routing logic needed. This keeps the framework's conversation loop unchanged.
  • Synchronous delegation — the calling agent blocks on the tool result, same as any other tool. No need for async messaging or callbacks.
  • Team-scoped — delegation only works within the same team. Cross-team delegation is out of scope for now.
  • Depth limit — add a maxDelegationDepth (default: 3) to prevent infinite delegation chains (agent A delegates to B which delegates to A).
  • Concurrency — delegation runs through the same AgentPool semaphore, so it respects the team's maxConcurrency setting. Deadlock risk: if all slots are occupied by agents waiting on delegations, the pool deadlocks. Mitigation: document this constraint, and optionally reserve one slot for delegations.

Deadlock prevention

When agent A delegates to agent B, A holds a pool slot while waiting. If all N slots are held by delegating agents, the system deadlocks. Two mitigation options (can start with the simpler one):

  1. Documentation — note that maxConcurrency should be > max delegation depth
  2. Dedicated delegation slots — reserve ceil(maxConcurrency / 2) slots for top-level tasks, allow delegations to exceed the cap

Acceptance Criteria

  • delegate_to_agent tool registered in tool/built-in/delegate.ts
  • Tool receives target agent name and task description, returns agent output
  • Works within runTeam() and runTasks() flows (team context available)
  • Graceful error when target agent doesn't exist or is the caller itself
  • Delegation depth limit prevents infinite loops (configurable, default 3)
  • Delegation results written to SharedMemory for audit trail
  • onTrace emits trace events for delegated agent runs
  • Tests cover: successful delegation, agent-not-found error, self-delegation error, depth limit, SharedMemory write
  • Example added: examples/14-agent-handoff.ts

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Implement later — still lightweightenhancementNew feature or requestsource:competitiveSource: competitive analysis

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions