Delegation and tools
Who does the work the AI persona hands off, and the round trip that runs your tools.
The AI persona talks with the user. When the user needs work done, it hands the work off and keeps talking, and says the result when it comes. The token's interface_setup.delegation says who does that work:
type | Who does the work | Your tools |
|---|---|---|
client (the default) | Your page: it does the task, and tells the persona how it went. | Your page runs whatever it likes. |
responses | A backend reasoner on our side, with your instructions. | The backend calls your function tools; your page runs them and sends back the results. |
none | Nobody: the persona hands nothing off. | No hand-off reaches your page. |
With client or responses, say in the persona's prompt (personality.system_prompt) which requests to hand off: it decides on its own, and hands off when its instructions say to.
Client delegation
With no delegation in the token, or {"type": "client"}, a hand-off reaches your page as session.delegation.created with target: "client". Your page works out the task from what the user said, does it, and reports back with appends that name the delegation:
{ "type": "session.delegation.created", "event_id": "evt_12", "offset_ms": 5230, "delegation": { "id": "del_1", "type": "delegation", "target": "client" } }
{ "type": "session.thinking.append", "delegation_id": "del_1", "content": "Checking tables for four at 7 pm." }
{ "type": "session.commentary.append", "delegation_id": "del_1", "content": "The table for four at 7 pm is booked." }session.thinking.appendwith the delegation's id is progress: the persona may mention it, and the task stays open.session.commentary.appendwith the delegation's id is the result: the persona tells the user in its own words, and the task is done.- Under client delegation,
response.item.createis refused (delegation_type_mismatch): typed words and tool results need a responses delegation.
No delegation
With {"type": "none"} the persona hands nothing off: no session.delegation.created reaches your page, and it answers from its prompt, what it knows and its own tools. Use it when nothing on your side would answer a hand-off.
Responses delegation
With type: "responses", a backend reasoner does the work, in steps, and calls your tools:
{
"type": "responses",
"responses": {
"model": "default",
"instructions": "Use get_weather for any weather question, then answer in one short sentence.",
"tools": [
{
"type": "function",
"name": "get_weather",
"description": "Looks up the current weather in a city.",
"parameters": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] }
}
],
"reasoning": { "effort": "low" }
}
}| Field | What it is |
|---|---|
model | Required: any non-empty name up to 256 characters, such as "default". |
instructions | The backend's instructions, up to 65,536 characters, which only the backend sees. Put large context known at the start here, such as a catalog. |
tools | Your function tools (below). A session runs at most 16. |
tool_choice | auto (the default), none, required, or {"type": "function", "name": …}. |
parallel_tool_calls | true (the default) lets a step make several calls; false, one per step. |
max_output_tokens | The most a step writes, from 16. Reasoning tokens count toward it. |
reasoning.effort | How much the backend thinks before each step: none, minimal, low (the default), medium, high or xhigh. Lower answers faster but guesses more. |
A key not listed here is refused at mint with 422 interface_setup, naming it.
Your tools
Each tool is a function in this flat shape. One with its fields nested under function is refused.
type:"function". A tool of another type is dropped.name: letters, digits,_and-, up to 64 characters, unique in the list. The persona's own tools' names are taken:ask_user,call_time,do_expression,do_gaze,get_current_time,stay_quiet,switch_identity,web_extractorandweb_search.description: when to use it, up to 2,000 characters.- Optional:
parameters, a JSON Schema whosetypeis"object", up to 16,000 characters as compact JSON (left out, the tool takes no arguments);strict,policyandannotations.
The list may hold up to 128 tools, but the session runs only the first 16 that keep these rules, and drops the rest. The token's warnings name each dropped tool and why, and they are the only report of it: the session never sees a dropped tool. Act on warnings when you mint. List at mint every tool the session may need: an update can't add one.
The tool round trip
In a web page, the JavaScript SDK does this round trip for you with onToolCall and sendToolResult.
A hand-off starts session.delegation.created with target: "responses". Each step of the backend's work then streams as response.event, with the backend's own event nested in event:
{ "type": "session.delegation.created", "event_id": "evt_20", "offset_ms": 5230, "delegation": { "id": "del_1", "type": "delegation", "target": "responses", "response_id": "resp_del_1_step_1" } }
{ "type": "response.event", "event_id": "evt_21", "delegation_id": "del_1", "event": { "type": "response.created", "response": { "id": "resp_del_1_step_1", "status": "in_progress" } } }
{ "type": "response.event", "event_id": "evt_24", "delegation_id": "del_1", "event": { "type": "response.output_item.done", "item": { "type": "function_call", "call_id": "del_1_c1", "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" } } }
{ "type": "response.event", "event_id": "evt_25", "delegation_id": "del_1", "event": { "type": "response.completed", "response": { "id": "resp_del_1_step_1", "status": "completed" } } }The nested events are shortened here: each also has a sequence_number, and more fields than shown.
your page Nuance Live
│ ◄── session.delegation.created ───────────────────────────│ the persona hands a task off
│ ◄── response.event: function_call ────────────────────────│ one for each call of the step
│ ◄── response.event: response.completed ───────────────────│ the step is complete
│ run each tool │
│ ── response.item.create (one for each call) ─────────────►│
│ ── response.create ──────────────────────────────────────►│ the task goes on
│ ◄── response.event: the answer (phase final_answer) ──────│ the persona says it-
Collect each
function_callitem fromresponse.output_item.done, keyed bydelegation_id: a task has one step open at a time.argumentsis a JSON string. -
Run your tool. Check the arguments against its
parametersfirst: the backend fills them in from the conversation, and they reach you as it wrote them. -
Answer once the step's
response.completedhas come and every call of it has a result: oneresponse.item.createper call, then oneresponse.create.{ "type": "response.item.create", "item": { "type": "function_call_output", "call_id": "del_1_c1", "output": "{\"sky\": \"light rain\", \"temperature_c\": 14}" } } { "type": "response.create" } -
The backend goes on: it makes more calls, or answers. Its text streams as
response.output_text.deltaand.done, in an assistantmessageitem whosephaseisfinal_answerfor the task's answer, orcommentaryfor text beside calls. The persona says the answer in its own words.
| Nested event | Meaning |
|---|---|
response.created | A step started. |
response.output_item.done | An item is complete: a function_ to answer, or an assistant message whose phase is commentary or final_. |
response., response. | A message's text as it streams, with the item's item_. |
response.completed | The step is complete. |
response.failed | The step failed: see When a task fails. |
Read the calls and the answer from the item events, not from response.completed: its response shows output: []. Other events of the stream arrive too; ignore the ones you don't use.
- Results.
outputis a string of up to 24,000 characters; JSON text stays JSON for the backend. A longer one is refused (string_above_max_length), and the call waits for a shorter one. For images, sendoutputas parts:input_textparts and up to 4input_imageparts, each a base64data:URL of a JPEG or PNG inimage_url. The persona and the backend see them; each is scaled to fit 1,920 px. Remote URLs and file ids are refused. - A call that failed answers with a JSON error, such as
{"error": {"message": "Card declined"}}: the backend reads it and may try again, and if the task ends with no answer, the persona tells the user it wasn't done, with the error'smessage. An object whoseok(or, with nook,success) isfalse, beside nothing butmessage,reason,whyorcode, is a failure too, such as{"ok": false, "why": "sold_out"}. Other JSON, such as{"status": "failed"}, and plain text count as calls that went through, whatever they say. - Every call waits for its result for as long as the session lasts, so a slow tool is fine; the persona keeps talking meanwhile. A result for an unknown call, or a second one for the same call, is refused (
invalid_value). Aresponse.createthat can resume no task while a call still waits is refused withfunction_call_outputs_required. - Only your tools reach you. Calls that the persona or the backend make to the persona's own tools never do.
While the backend works
- With nobody speaking for 3.5 seconds while a task runs, the persona says one short line on where it stands. An empty
session.thinking.append(delegation_id: null) turns that line off for the tasks under way. - When the persona's reply ends on a question, an answer it has yet to say waits for the user's reply, and its answer to them says it. If they say nothing, it gets a turn of its own after about 5 seconds.
Tools that aren't available right now
When an action can't run in your app's current state, take its tool out with session.update, and send it back when it can. tools is replaced whole: send every tool that can run now, each exactly as you listed it at mint (key order doesn't matter). The backend is offered only those from its next step on, and the persona is told what the backend can do now, so it can tell the user an action isn't possible yet. A step already under way when the update lands can still call a tool you took out, and that call reaches you: answer it as usual, for example with an error. A line in a prompt or an append saying "not available now" does none of this.
{ "type": "session.update", "session": { "delegation": { "type": "responses", "responses": { "tools": [{ "type": "function", "name": "get_weather", "description": "Looks up the current weather in a city.", "parameters": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] } }] } } } }Send the definitions you minted, never the room's echo of them: the echo names each tool without its schema, and an update built from it is refused with immutable_field_update.
Tools that change something
annotations takes MCP's hints, up to 2,000 characters as compact JSON. A tool whose readOnlyHint and idempotentHint are both false or left out (such as {"destructiveHint": true}) is called at most once per user request with the same arguments, across all the session's tasks. Annotate tools that send, book, pay or post this way, so two tasks working on one request don't both do it. A tool with no annotations runs every call.
Tool policies
"policy": {"confirm": true}: the persona reads the call back to the user and sends it only after their clear yes. Use it for anything that can't be undone."policy": {"safety_resource": true}: for a tool that gives a crisis or safety resource, such as an emergency number or the 988 Suicide and Crisis Lifeline. The persona must say every number of three or more digits in the backend's answer after it, in order, and the answer gets its turn at once. The phone-number check below applies to its results too: another helpline's number in them can end the session.
A phone number in any of your tools' results or anything the backend writes can end the session for a policy violation (session.closed with reason: "content"), whether or not the persona would say it, and that end counts toward the pauses after repeated violations. Only emergency numbers and a short fixed list of US national crisis lines, 988 among them, pass; any other helpline's number can end the session too, including other countries' lines. Keep phone numbers out of results, including your own business's number and a number your calling tool dialed, and refer to a contact by an id.
When a task fails
A step that fails ends with response.failed inside response.event, its response.error.code saying why. The persona tells the user it couldn't finish, and may hand off again. These say the backend didn't run the step:
response.error.code | What happened | Try again |
|---|---|---|
backend_unreachable | It wasn't there to answer, or stopped answering part way. | Yes |
backend_unavailable | Nothing was there to run the session's tasks. | Yes; tell us if it keeps happening |
backend_capacity | It's full right now. | Yes, shortly |
backend_refused | It said no to this task. | No: it fails the same way |
backend_error | It had the task and couldn't finish it. | Maybe |
server_error | A fault of ours. | Maybe |
These say the task ended without an answer: no_answer, call_as_text (the backend wrote a call as text instead of making it), voice_turn (it wrote the persona's turn instead of an answer), too_many_steps (a task gets 8 steps that may make calls, then one to answer), deadline (60 seconds of backend time, not counting your tools' time) and abandoned (nobody waits for it any more). A task the session withdraws before any call went out ends response.failed with status cancelled, and an info with code delegation_cancelled.