Skip to content

For clean Markdown of any page, append .md to the page URL. For a complete documentation index, see For full documentation content, see For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at

Tool calling

For the complete documentation index, see llms.txt

Give your agent the ability to look up information, call APIs, or trigger workflows by registering tools in session.tools. The agent decides when to invoke them and emits a tool.call; you execute the tool and reply with a tool.result. Wait until reply.done before sending it back.

Registering tools

Register tools by passing an array of tool definitions in session.tools on a session.update event. Each tool uses a flat format with type, name, description, and parameters at the top level.

json
{
  "type": "session.update",
  "session": {
    "system_prompt": "You are a helpful assistant. Use get_weather for weather questions.",
    "greeting": "Hi! How can I help?",
    "tools": [
      {
        "type": "function",
        "name": "get_weather",
        "description": "Get the current weather for a city.",
        "parameters": {
          "type": "object",
          "properties": {
            "location": {
              "type": "string",
              "description": "City name, e.g. London"
            }
          },
          "required": ["location"]
        }
      }
    ]
  }
}

You can update session.tools mid-session by sending another session.update. The new array replaces the previous one.

Handling tool calls

The key pattern: accumulate tool results, then send them all in reply.done, not immediately in tool.call. The agent speaks a transition phrase while waiting; sending results too early can cause timing issues.

python
pending_tools: list[dict] = []

# In your event loop:

if t == "tool.call":
    name = event["name"]
    arguments = event.get("arguments", {})   # arguments is a plain dict

    # Execute your tool
    if name == "get_weather":
        result = {"temp_c": 22, "description": "Sunny"}
    else:
        result = {"error": "Unknown tool"}

    # Accumulate - don't send yet
    pending_tools.append({
        "call_id": event["call_id"],
        "result": result,
    })

elif t == "reply.done":
    if event.get("status") == "interrupted":
        # User barged in - discard pending results
        pending_tools.clear()
    elif pending_tools:
        # Now send all tool results
        for tool in pending_tools:
            await ws.send(json.dumps({
                "type": "tool.result",
                "call_id": tool["call_id"],
                "result": json.dumps(tool["result"]),
            }))
        pending_tools.clear()

If a tool call arrives and the user then interrupts the agent before reply.done completes normally, discard the pending tool results and wait for the next turn. Sending stale results can confuse the agent's state.

What the agent does while tools run

While waiting for tool.result, the agent generates a short transition phrase based on the system prompt (for example, "Let me check that for you"). This keeps the conversation flowing naturally rather than leaving dead air. The phrasing is influenced by the system prompt, so you can steer it by including instructions like "While looking up information, say 'One moment'".

If the user interrupts during the transition phrase, you'll receive reply.done with status: "interrupted". Discard any pending tool results. See Handling interruptions for the full pattern.