Nuance LabsDocs

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:

typeWho does the workYour tools
client (the default)Your page: it does the task, and tells the persona how it went.Your page runs whatever it likes.
responsesA backend reasoner on our side, with your instructions.The backend calls your function tools; your page runs them and sends back the results.
noneNobody: 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.append with the delegation's id is progress: the persona may mention it, and the task stays open.
  • session.commentary.append with 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.create is 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" }
  }
}
FieldWhat it is
modelRequired: any non-empty name up to 256 characters, such as "default".
instructionsThe 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.
toolsYour function tools (below). A session runs at most 16.
tool_choiceauto (the default), none, required, or {"type": "function", "name": …}.
parallel_tool_callstrue (the default) lets a step make several calls; false, one per step.
max_output_tokensThe most a step writes, from 16. Reasoning tokens count toward it.
reasoning.effortHow 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_extractor and web_search.
  • description: when to use it, up to 2,000 characters.
  • Optional: parameters, a JSON Schema whose type is "object", up to 16,000 characters as compact JSON (left out, the tool takes no arguments); strict, policy and annotations.

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
  1. Collect each function_call item from response.output_item.done, keyed by delegation_id: a task has one step open at a time. arguments is a JSON string.

  2. Run your tool. Check the arguments against its parameters first: the backend fills them in from the conversation, and they reach you as it wrote them.

  3. Answer once the step's response.completed has come and every call of it has a result: one response.item.create per call, then one response.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" }
  4. The backend goes on: it makes more calls, or answers. Its text streams as response.output_text.delta and .done, in an assistant message item whose phase is final_answer for the task's answer, or commentary for text beside calls. The persona says the answer in its own words.

Nested eventMeaning
response.createdA step started.
response.output_item.doneAn item is complete: a function_call to answer, or an assistant message whose phase is commentary or final_answer.
response.output_text.delta, response.output_text.doneA message's text as it streams, with the item's item_id.
response.completedThe step is complete.
response.failedThe 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. output is 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, send output as parts: input_text parts and up to 4 input_image parts, each a base64 data: URL of a JPEG or PNG in image_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's message. An object whose ok (or, with no ok, success) is false, beside nothing but message, reason, why or code, 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). A response.create that can resume no task while a call still waits is refused with function_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.codeWhat happenedTry again
backend_unreachableIt wasn't there to answer, or stopped answering part way.Yes
backend_unavailableNothing was there to run the session's tasks.Yes; tell us if it keeps happening
backend_capacityIt's full right now.Yes, shortly
backend_refusedIt said no to this task.No: it fails the same way
backend_errorIt had the task and couldn't finish it.Maybe
server_errorA 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.

On this page