Routing & Handoffs
When a conversation can go to multiple specialists, you need a router. Kuralle's routes + routing setup handles the decision and the handoff without surfacing triage logic to the user.
Defining a router
Section titled “Defining a router”Add routes and routing to defineAgent:
import { openai } from '@ai-sdk/openai';import { defineAgent } from '@kuralle-agents/core';
const triage = defineAgent({ id: 'triage', routes: [ { agent: 'billing', when: 'billing question or payment issue' }, { agent: 'support', when: 'general product support or anything else' }, ], routing: { model: openai('gpt-4o-mini') },});Each routes entry needs a when description and a target — either an agent (a destination agent's id) or a flow (a flow on this agent). The model uses the when descriptions to select the right route.
Derived routing behavior
Section titled “Derived routing behavior”Routing is derived from which fields you populate — there is no routing.mode:
- Pure dispatcher —
routes/agents/handoffsonly, with no local answering surface (noinstructions,tools,flows, etc.). The runtime runs a silent internal classifier and dispatches without speaking. - Answering agent — has
instructions,flows,tools, or other local affordances. Host-control tools (enter_flow,transfer_to_agent) fold routing into the speaking turn; a lazy model-reasoned guard runs only when the turn produces no answer and no control tool, catching the case where the model neither answered nor routed.
Set routing: { model } to classify with a separate (e.g. cheaper) control model. For compliance text that must never stream before dispatch resolves, set routing: { dispatch: 'strict' }.
Model every fallback as a normal route with a semantic when (e.g. "general support") — there is no routing.default.
Handoffs
Section titled “Handoffs”A handoff moves the conversation to another agent and carries full session context. The receiving agent picks up with the complete message history.
Silent handoffs (default)
Section titled “Silent handoffs (default)”A handoff should read as one continuous assistant — the user shouldn't see a "transferring you…" line or the target re-introducing itself. Kuralle makes this the default:
- No transfer announcement (deterministic): the transfer is a silent control tool call — the runtime never emits a "please hold / connecting you" message.
- Continuation nudge (best-effort): the target is given a directive not to greet or re-introduce itself, so it continues seamlessly. This mirrors the OpenAI Agents SDK's history-nesting approach.
createRuntime({ agents, defaultAgentId, silentHandoff: false }); // opt into a visible transferFor tool-based handoffs — where the agent decides explicitly when to transfer — declare which agent IDs the agent can hand off to with handoffs, and wire a handoff tool:
const transferToSpanish = defineTool({ name: 'transfer_to_spanish', description: 'Transfer to a Spanish-speaking agent when the user requests Spanish.', input: z.object({ reason: z.string() }), execute: async ({ reason }) => ({ __handoff: true, targetAgentId: 'spanish-agent', reason, }),});
const englishAgent = defineAgent({ id: 'english', instructions: 'Friendly assistant. If the user speaks Spanish, use transfer_to_spanish.', model: openai('gpt-4o-mini'), handoffs: ['spanish-agent'], agents: [spanishAgent], tools: { transfer_to_spanish: transferToSpanish },});The runtime intercepts { __handoff: true, targetAgentId } in the tool result and routes the session to the target agent. Kuralle also ships a createHandoffTool helper that builds this transfer tool for you.
Example: triage router
Section titled “Example: triage router”import { openai } from '@ai-sdk/openai';import { defineAgent, createRuntime } from '@kuralle-agents/core';
const billingAgent = defineAgent({ id: 'billing', instructions: 'You handle billing questions and payment issues.', model: openai('gpt-4o-mini'),});
const supportAgent = defineAgent({ id: 'support', instructions: 'You handle product support requests.', model: openai('gpt-4o-mini'),});
const triage = defineAgent({ id: 'triage', routes: [ { agent: 'billing', when: 'billing question or payment issue' }, { agent: 'support', when: 'product support or general help' }, ], routing: { model: openai('gpt-4o-mini') },});
const runtime = createRuntime({ agents: [triage, billingAgent, supportAgent], defaultAgentId: 'triage', // A pure dispatcher declares no `model` of its own, and the runtime's model // gate reads `agent.model ?? defaultModel` — it does not fall back to // `routing.model`. Without this line every turn throws before classifying. defaultModel: openai('gpt-4o-mini'),});