Skip to main content

What is a handler?

A handler is a function that takes one side’s turns in a conversation. When a handler backs a side of an agent, the runtime calls the handler once for each inbound message. The runtime sends no model request for that side. A handler has one role: it backs a side of an agent. Three other definitions cover the remaining roles:
  • A hook changes or observes a turn that a prompt or a handler owns.
  • A tool is a capability that a prompt chooses to call.
  • An effect runs outside the conversation flow.

Defining a handler

Create a file in agents/handlers/ and export a defineHandler call as the default export. The file name is the handler name: agents/handlers/relay_pipe.ts defines the handler relay_pipe.
message is the complete persisted message, with its id, role, content, silent flag, metadata, and timestamps.

Referencing a handler

Reference a handler from the handler property of a side in defineAgent. This is the only place that can reference a handler.
A handler-backed side accepts handler, label, and subagents. It does not accept maxSteps, stopOnResponse, stopTool, or the session bindings, because a handler turn has no steps. Standard Agents Builder rejects a prompt whose tools or hooks array names a handler. The error is raised before the prompt’s side runs.

Execution model

A turn of a handler-backed side has three stages:
  1. An inbound message arrives on the side’s perspective. Side A acts on user messages. Side B acts on assistant messages.
  2. The runtime calls execute once with the thread’s ThreadState and that message.
  3. execute returns or throws. The turn is complete.
When the agent sets maxSessionTurns, a handler turn counts toward that limit in the same way as a prompt turn. Each handler call writes one row to the thread’s logs. The row names the handler and records its duration.

Delivery

Standard Agents Builder delivers each inbound message to the handler one time:
  • The handler runs once for each inbound message, in the order the messages were stored.
  • One thread never runs two handler calls at the same time. A message that arrives during a handler call waits and is delivered after that call returns.
  • A message whose handler call failed is not delivered again. Send the message again to retry.
  • The runtime records the delivery position before each call. A restart during a call does not deliver that message a second time.
A handler that runs for a long time delays every later message on its thread. A handler that waits for a blocking child session holds the thread for the whole session. If that child can finish only after another message arrives on this thread, the thread stops making progress. Forward with queueMessage() and handle the child’s reply as a later turn.

Errors

A handler that throws fails that delivery only:
  • The log row for the call records the error with error_type handler_execution_error and a runtime origin.
  • The inbound message stays stored. Nothing is rolled back.
  • The session continues. The next inbound message starts a new handler call.
To end the session after an error, catch the error and call state.stopSession().

What a handler can do

execute receives the same ThreadState that tools receive:
  • Forward: (await state.getChildThread(reference))?.queueMessage(...) sends a message to a child. state.notifyParent(...) sends one to a parent.
  • Respond: state.injectMessage({ role, content }) writes a message in the side’s own role. The role is assistant for side A and user for side B.
  • Use children: the side’s subagents declarations make child instances available through state.children. A handler needs no lifecycle tools.
  • Control: state.stopSession(), state.setStatus(), state.scheduleEffect(), the environment, the thread values, and the file system.
The runtime gives a handler no reply channel of its own. A handler that must reach a person or an external system calls that system in its own code.

Avoiding message loops

A handler runs only when a message arrives. A handler that replies to every inbound message can still exchange messages with a prompt-backed side without end. Use one of these controls:
  • Reply only when a condition holds.
  • Set maxSessionTurns on the agent.
  • Call state.stopSession() when the work is done.

Next steps

Agents

Choose what backs each side of an agent

Subagents

Declare the child relationships a handler forwards to

ThreadState

See the state object a handler receives

Effects

Schedule work that runs outside a turn