Skip to content

Multi-agent delegation

In Genkit, multi-agent systems split work between specialized agents and an orchestrator. The orchestrator decides which specialist should handle each part of the request, then synthesizes a final answer.

Use this pattern when separate capabilities benefit from separate prompts, tools, state, or evaluation. A single agent with several tools is usually simpler when one prompt can coordinate the whole task. Multiple agents are useful when specialists need different instructions, different model settings, durable specialist memory, or independently inspectable artifacts.

The middleware package provides agents() for delegation. It injects one delegation tool per sub-agent. By default, tool names use delegate_to_<agentName>.

import { agents, artifacts, retry } from '@genkit-ai/middleware';
const researcher = ai.defineAgent({
name: 'researcher',
description: 'Finds facts and produces sourced research notes.',
system: 'Research the user request and write concise findings.',
use: [artifacts(), retry()],
});
const coder = ai.defineAgent({
name: 'coder',
description: 'Writes and explains code.',
system:
'Write clear TypeScript code unless the user asks for another language.',
use: [artifacts(), retry()],
});
const coordinator = ai.defineAgent({
name: 'coordinator',
system:
'Delegate to specialists, inspect their results, then answer the user.',
use: [
agents({
agents: [
'researcher',
{
name: 'coder',
description:
'Writes, debugs, and explains code. Use for programming tasks.',
},
],
historyLength: 4,
maxDelegations: 5,
artifactStrategy: 'session',
}),
artifacts({ readonly: true }),
],
});

The middleware can discover agent descriptions from action metadata, or you can override a description in the middleware config. Keep descriptions concrete because they become tool descriptions for the orchestrator model.

  • agents accepts agent names, agent actions, or entries with a name and description override.
  • toolPrefix controls generated tool names. It defaults to delegate_to; set it to an empty string to use bare agent names.
  • historyLength sets how many recent conversation messages are forwarded to sub-agents.
  • maxDelegations limits delegation calls in one orchestrator turn.
  • artifactStrategy controls whether sub-agent artifacts are merged into the parent session.
  • async enables background delegation.
  • maxWaitSeconds caps how long one wait_for_background_tasks call blocks, whatever timeout the model asks for.

When history is forwarded to client-managed sub-agents, the middleware includes recent messages in the sub-agent state. For server-managed sub-agents, history is not forwarded as client state because those agents own their server-side session.

By default a delegation blocks the orchestrator’s turn until the sub-agent finishes. With async: true, every delegation tool also accepts background: true. A background delegation starts the sub-agent as a detached task and returns a task ID right away, so the orchestrator can keep working and collect the result later.

import { agents, retry } from '@genkit-ai/middleware';
import { InMemorySessionStore } from 'genkit/beta';
const logAnalyst = ai.defineAgent({
name: 'logAnalyst',
description: 'Reads service logs and reports what they show.',
store: new InMemorySessionStore(), // required for background work
tools: [readLogs],
});
const deployHistorian = ai.defineAgent({
name: 'deployHistorian',
description: 'Lists recent deploys and what changed in each.',
store: new InMemorySessionStore(),
tools: [listDeploys],
});
const commander = ai.defineAgent({
name: 'commander',
system:
'Start investigations in the background, post a status update while they run, then collect the results.',
tools: [postStatus],
maxTurns: 20,
use: [
agents({ agents: ['logAnalyst', 'deployHistorian'], async: true }),
retry(),
],
});

async adds three tools for managing background tasks:

ToolWhat it does
check_background_tasksReports the current status of the given tasks without waiting.
wait_for_background_tasksWaits until the tasks settle. Accepts timeoutSeconds and waitFor: "first".
abort_background_tasksStops the given tasks without waiting. A running task reports aborting.

A stopped task later settles as aborted with the progress it saved, and wait_for_background_tasks collects it. A task that had already finished or expired is left alone and reports its status.

A task ID has the form <agent>:<snapshotId> and comes back in the delegation tool’s result, so it lives in the conversation history. An orchestrator rebuilt from that history can still collect the result. Background delegation requires a sub-agent with a session store. A launch on a store-less agent is refused, and the orchestrator is told to delegate synchronously instead.

Background work adds tool rounds (launch, post, wait, post, wait again), so raise maxTurns on the orchestrator.

A delegation to a sub-agent with a session store leaves a handle behind when it settles, whether it ran in the background or not: the result carries a taskId and a status of completed, failed, or aborted. The continue_task tool picks the task up from that handle:

continue_task { taskId, instructions?, background? }
  • Failed or aborted: continues from the sub-agent’s last saved progress. Without instructions, it retries as it stood. With them, it steers the retry.
  • Completed: takes follow-up instructions inside the sub-agent’s own session. It is refused without them, since an empty retry would only repeat the finished work.
  • Expired (its background worker is presumed dead): stops the stale worker first, then continues from the last snapshot committed before the background run started. A fresh background delegation has no such snapshot, so it is refused and the orchestrator is told to delegate again. It is also refused when the sub-agent’s store cannot signal a running worker to stop.
  • Pending or aborting: refused, with a pointer to the wait or check tool.

A sub-agent run that stopped on an interrupt cannot be continued, since continuing it would mean answering the interrupt. A synchronous result carries no taskId, and continue_task refuses a background task that stopped on one. The orchestrator should delegate a more self-contained task instead.

background is accepted only with async: true. A continuation counts against maxDelegations. When every configured sub-agent is client-managed, the tool is hidden from the model.

Task handles are scoped to the conversation. The background-task tools and continue_task accept only task IDs that this conversation’s delegations and continuations returned, so text that reaches the model cannot point them at another conversation’s task. The sub-agent’s own getSnapshot, waitForSnapshot, and abort routes are not scoped, so in multi-tenant deployments treat snapshot IDs as secrets where those routes are exposed.

Only an explicitly set, non-empty toolPrefix namespaces these shared tools: toolPrefix: 'research' gives research_continue_task. The default delegate_to prefix does not, so the shared tools keep the bare names shown above. Two agents() instances in one call therefore need distinct, explicit prefixes. Otherwise both register continue_task, whether or not async is set, and the call is rejected for duplicate tool names.

Delegation appears as normal tool activity in the orchestrator stream.

const turn = coordinator
.chat()
.sendStream('Research sorting algorithms and write quicksort.');
for await (const chunk of turn.stream) {
for (const request of chunk.toolRequests) {
const name = request.toolRequest.name;
if (name.startsWith('delegate_to_')) {
showDelegation(name);
}
}
if (chunk.text) {
appendText(chunk.text);
}
}

Sub-agent interrupts and failures are returned to the orchestrator as tool output. They do not automatically become top-level interrupts for the original client. Write orchestrator instructions that tell it how to handle delegated failures, such as continuing the task with continue_task, choosing another specialist, or asking the user for clarification. An interrupted sub-agent run cannot be continued; the orchestrator has to delegate a more self-contained task.

With artifactStrategy: 'session', sub-agent artifacts are merged into the parent session and namespaced by invocation. Pair this with artifacts({ readonly: true }) so the orchestrator can inspect delegated work through the read_artifact tool.

Use session artifacts when delegated work should be visible to the final user or to later turns. Keep artifacts isolated when the specialist output is only an implementation detail for the orchestrator’s current answer.

The older Building multi-agent systems page describes a prompts-as-tools pattern. Prefer the Agents API middleware for new work because it integrates with sessions, streaming, persistence, background execution, and HTTP clients.