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.
Add delegation middleware
Section titled “Add delegation middleware”Genkit Dart provides the agents() middleware from package:genkit_middleware/agents.dart to manage multi-agent delegation. It dynamically auto-injects one delegation tool per sub-agent (named delegate_to_<agentName> by default) and appends a list of available sub-agents and their descriptions to the orchestrator’s system prompt.
Make sure to include AgentsPlugin() in your Genkit initialization.
The agents() middleware is experimental and pulls getCurrentSession from package:genkit/experimental.dart, which is also where defineAgent() lives.
import 'package:genkit/genkit.dart';import 'package:genkit/experimental.dart';import 'package:genkit_middleware/agents.dart';
final researcher = ai.defineAgent( name: 'researcher', description: 'A thorough research assistant that provides well-sourced answers.', system: 'You are a thorough research assistant. Return a clear and factual answer.', maxTurns: 10,);
final coder = ai.defineAgent( name: 'coder', description: 'Writes, debugs, and explains code.', system: 'You are an expert programmer. Use Dart by default.', maxTurns: 10,);
final orchestratorAgent = ai.defineAgent( name: 'orchestratorAgent', system: ''' You are a project assistant. Analyze the user's request and delegate to the appropriate sub-agent. If the request requires both research and code, call them sequentially. After receiving sub-agent responses, synthesize a final answer for the user. ''', use: [ agents( agents: ['researcher', 'coder'], maxDelegations: 5, historyLength: 4, ), ], store: InMemorySessionStore(),);Always provide a clear, descriptive description for sub-agents, as this metadata is used directly by the orchestrator model to determine when to call each delegation tool.
Delegation options
Section titled “Delegation options”agentsis a list of sub-agent names available to the orchestrator.toolPrefixcontrols generated tool names. It defaults todelegate_to(tools becomedelegate_to_<agent>); set it to an empty string to use bare agent names.maxDelegationscaps delegation calls in one orchestrator turn to prevent runaway loops (e.g.,5).historyLengthsets how many recent conversation messages are forwarded to the sub-agents so they have context.0or omitted forwards only the task description.artifactStrategycontrols how sub-agent artifacts surface:inline(default) includes artifact content in the delegation tool result and merges artifacts into the parent session;sessionmerges into the parent session only, so the tool result names the artifacts without their content.asyncenables background delegation. See Delegate in the background.
History is forwarded only to client-managed sub-agents (those without a session store). A server-managed sub-agent owns its server-side session, so it receives only the task description.
Stream delegation progress
Section titled “Stream delegation progress”Delegation appears as a standard tool call in the orchestrator’s chunk stream. This allows clients to see in real-time which sub-agent is active.
final turn = orchestratorAgent.chat().sendStream(text: 'Research quicksort and write it in Dart.');
await for (final chunk in turn.stream) { for (final req in chunk.toolRequests) { final name = req.toolRequest.name; if (name.startsWith('delegate_to_')) { print('Delegating to sub-agent: $name'); } } if (chunk.text.isNotEmpty) { stdout.write(chunk.text); }}Delegate in the background
Section titled “Delegate in the background”An orchestrator that delegates and waits is blocked for as long as the sub-agent runs. Set async: true to let it keep working instead. Each delegation tool then accepts a background flag that starts the sub-agent and returns a task ID at once, and three shared tools give the orchestrator one control per thing it can do with a launched task.
| Tool | Purpose |
|---|---|
check_background_tasks | Returns the current status of the given tasks without waiting. |
wait_for_background_tasks | Blocks until the given tasks settle. timeoutSeconds bounds the wait; waitFor: "first" returns as soon as one settles. |
abort_background_tasks | Stops tasks whose results are no longer needed. |
Background delegation requires server-managed sub-agents: the launched sub-agent needs a session store that supports detach, because background work is tracked by a pending snapshot. A sub-agent without one can only be delegated to synchronously, and the launch is refused with a hint to retry that way. The orchestrator itself needs no store for this.
import 'package:genkit/genkit.dart';import 'package:genkit/experimental.dart';import 'package:genkit_middleware/agents.dart';
// Server-managed: the store is what makes background delegation possible.final researcher = ai.defineAgent( name: 'researcher', description: 'Researches a single, focused topic and returns a concise summary.', system: 'You are a focused research assistant. Given one topic, return a tight summary.', store: InMemorySessionStore(), maxTurns: 4,);
final orchestrator = ai.defineAgent( name: 'asyncOrchestrator', system: ''' When the user asks about several topics, delegate each one to "researcher" in the background (set "background": true) so they run in parallel, keeping the taskId each delegation returns. Once every task is launched, call wait_for_background_tasks with all the taskIds to collect the results, then synthesize a final answer. ''', use: [ agents(agents: ['researcher'], async: true, maxDelegations: 8), ], store: InMemorySessionStore(),);The orchestrator launches with {"task": "...", "background": true}, calls other tools while the sub-agent runs, and collects the result later. wait_for_background_tasks takes an optional timeoutSeconds, so a slow task becomes an interim answer instead of a blocked turn: on timeout the current statuses are returned. An abort is safe on any task and never blocks: a task that already finished is left alone and reports its result, and a live one reports aborting while it saves progress, settling as a resumable aborted.
Task IDs ride in the tool result, so the orchestrator’s own conversation is the registry: task IDs from earlier tool results stay valid across turns, and re-delegating the same work is avoidable by checking them first. Because launching, collecting, and following up are separate tool rounds, raise maxTurns and maxDelegations so a background-collecting orchestrator has room to work.
The middleware explains how background delegation works, but not when to use it, so tell the orchestrator which delegations to background and when to post interim updates. The async agent sample is the worked example: an orchestrator that fans several research tasks out in the background and folds their answers in as they settle.
Continue a task
Section titled “Continue a task”Every delegation to a server-managed sub-agent leaves a continuable handle behind, named by the task’s taskId. The continue_task tool spends it:
- A failed or aborted task continues from its last saved progress. With no
instructionsthe committed turn is re-attempted as it stood; withinstructionsthe retry is steered by a fresh user message. - A completed task accepts follow-up
instructionsinside the sub-agent’s own session, so pressing on never repeats finished work. It is refused without them. - A task that stopped on an interrupt is refused as a dead end, since continuing it would mean answering the interrupt.
The tool registers only when a configured sub-agent may be server-managed, so an all-client-managed configuration gets no dead tool. With async: true, continue_task also accepts background: true and returns a fresh task ID in the same session.
Interrupts and failures
Section titled “Interrupts and failures”If a sub-agent fails or triggers an interrupt, the failure or pause is returned to the orchestrator as the delegation tool’s output. It does not automatically bubble up as a top-level error to the client. You should instruct the orchestrator on how to handle failures, for example by continuing the task with continue_task, trying a different specialist, correcting input, or reporting the issue back to the user.