Skip to navigation

Steer an active agent execution

Use x-runtype-concurrency: steer to send another message while your saved agent is working. If the conversation has an active execution, Runtype adds the input to that execution. If the conversation is idle, the message starts an execution.

Choose the admission policy that matches your application:

PolicyActive conversationTypical use
rejectReturns a busy error.Avoid overlapping requests.
queueStarts a later execution.Keep requests separate.
supersedeCancels and replaces the execution.Replace the active request.
steerAdds input to the active execution.Send additional information.

The default policy is reject. A conversation holds at most one waiting execution, so a second queued request returns CONVERSATION_QUEUE_FULL.

Steer never cancels the active execution. The in-flight model or tool call finishes first, and the agent sees the input at the next safe boundary. Other frameworks use “steer” for different behaviors. For a mapping, see Concurrency modes across agent frameworks.

Before you begin

Steering requires the following:

  • An agent that you created and saved in Runtype. Inline agent definitions and Claude Managed agents don’t support steering.
  • A turn that runs durably. Don’t set durability.forced to "in_process" on the agent or on the request. For details, see Control stream lifetime.
  • A stable conversationId that you send with every message in the conversation.
  • A Runtype API key in the RUNTYPE_API_KEY environment variable. The key needs DISPATCH:* to send through POST /v1/dispatch, or AGENTS:EXECUTE to send through POST /v1/agents/{agentId}/execute. For key permissions, see Authentication.

Use POST /v1/agents/{agentId}/execute when the agent uses client tools or can reach an approval gate. Dispatch doesn’t support those agents for steering and returns STEER_UNSUPPORTED_LANE. Dispatch also returns that error when the request carries dispatch-only inputs, such as secrets, inputs, or credential proxies.

To let Runtype choose between steering a message and queueing it as the next turn, see Route a busy message automatically.

Send only additional user messages

Send the same agent, conversationId, tenant and end-user identity, API key, and execution options on every message. Send only the new user messages, not the full transcript or replacement system instructions. Don’t combine steer with x-runtype-coalesce. Use a different idempotency key for each logical message.

To send additional input asynchronously with the TypeScript SDK, use this example:

TypeScript SDK
import { RuntypeClient } from '@runtypelabs/sdk'
const client = new RuntypeClient({ apiKey: process.env.RUNTYPE_API_KEY! })
const receipt = await client.dispatch.executeAsync(
{
agent: { agentId: 'agent_YOUR_AGENT_ID' },
conversationId: 'conversation-123',
messages: [{ role: 'user', content: 'Also compare the delivery times.' }],
},
{ concurrency: 'steer', idempotencyKey: 'user-message-42' }
)
console.log(receipt.executionId, receipt.deliveryId)

Replace agent_YOUR_AGENT_ID with your saved agent’s ID.

The Python example uses a dedicated client for this message. This keeps its idempotency header separate from other sends:

Python SDK
import os
from runtype import RuntypeClient
with RuntypeClient(
api_key=os.environ["RUNTYPE_API_KEY"],
headers={
"Prefer": "respond-async",
"x-runtype-concurrency": "steer",
"Idempotency-Key": "user-message-42",
},
) as client:
receipt = client.dispatch(
{
"agent": {"agentId": "agent_YOUR_AGENT_ID"},
"conversationId": "conversation-123",
"messages": [
{"role": "user", "content": "Also compare the delivery times."}
],
},
stream=False,
)
print(receipt["executionId"], receipt["deliveryId"])

To send the same input with cURL, use this request:

cURL
curl https://api.runtype.com/v1/dispatch \
-H "Authorization: Bearer $RUNTYPE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Prefer: respond-async' \
-H 'x-runtype-concurrency: steer' \
-H 'Idempotency-Key: user-message-42' \
-d '{
"agent": { "agentId": "agent_YOUR_AGENT_ID" },
"conversationId": "conversation-123",
"messages": [
{ "role": "user", "content": "Also compare the delivery times." }
],
"options": { "streamResponse": false }
}'

To steer through the saved-agent endpoint, put the agent ID in the path and omit the agent wrapper:

cURL: saved-agent endpoint
curl https://api.runtype.com/v1/agents/agent_YOUR_AGENT_ID/execute \
-H "Authorization: Bearer $RUNTYPE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Prefer: respond-async' \
-H 'x-runtype-concurrency: steer' \
-H 'Idempotency-Key: user-message-43' \
-d '{
"conversationId": "conversation-123",
"messages": [
{ "role": "user", "content": "Only include next-day options." }
],
"streamResponse": false
}'

The TypeScript SDK takes the same admission options, concurrency and idempotencyKey, as the last argument of these methods:

  • client.dispatch.execute(request, options)
  • client.agents.execute(agentId, request, options)
  • client.agents.executeAsync(agentId, request, options)
  • client.agents.executeStream(agentId, request, init)
  • client.dispatch.executeStream(request, init)

Track the delivery, not a separate execution

An asynchronous response includes executionId, deliveryId, deliveryStatus, and deliveryStatusUrl. Multiple deliveries can name the same execution.

To check a delivery, send GET to the deliveryStatusUrl, which has the form /v1/executions/{executionId}/deliveries/{deliveryId}. The API key needs AGENTS:READ, AGENTS:EXECUTE, or DISPATCH:*. With the TypeScript SDK, use client.executions.getDelivery(executionId, deliveryId). The response is a delivery receipt:

Delivery receipt
{
"deliveryId": "ain_01m1yr2k7xq3f9a8b6c4d2e0gh",
"executionId": "aex_01m1yr0hf1e65vpnb0zp8qj8jm",
"status": "settled",
"sequence": 2,
"initial": false,
"outcome": "completed"
}

The receipt names its state status. The asynchronous admission response names the same value deliveryStatus. The outcome field appears after the execution ends. A 404 response means the receipt is unknown or has expired.

The initial field is true when this delivery started its execution. It’s false when the delivery joined an execution that was already starting or running. To confirm that a steer landed on the execution you expected, compare executionId values instead of timestamps.

Use the delivery state to tell acceptance apart from use:

  • pending: Runtype accepted the input, but the agent hasn’t seen it yet.
  • applied: The input is part of the agent’s context. This doesn’t promise that the model follows the message.
  • settled: The execution ended. The receipt carries the execution’s outcome, including failure or cancellation.
  • not_applied: The execution ended before the agent saw the input. Runtype doesn’t restart it automatically.

To recover a delivery receipt, retry the same message with the same idempotency key. If you reuse that key with different messages or options, Runtype returns DELIVERY_IDEMPOTENCY_CONFLICT. Runtype keeps receipts for a limited time. They aren’t a permanent transcript archive.

Without Prefer: respond-async, blocking and server-sent events (SSE) callers follow the execution’s answer. SSE responses carry an X-Runtype-Delivery-Id header and replay the execution’s event stream. To avoid displaying the same answer twice, merge events by execution ID and event cursor.

Steer a conversation that answered busy

With the default reject policy, an overlapping send returns 409 with code CONVERSATION_BUSY. The body names the active execution and its phase:

{
"error": "This conversation already has a running turn. Retry after it settles.",
"code": "CONVERSATION_BUSY",
"executionId": "aex_01m1yr0hf1e65vpnb0zp8qj8jm",
"phase": "claiming"
}

The phase field has one of these values:

  • claiming: The execution is starting. A steer sent now is added when the execution starts.
  • running: The execution is working. A steer is added at the next safe boundary.
  • paused: The execution is waiting for an approval, client-tool result, or other resume. A steer is added after the resume.
  • queued: Another request is already waiting in the queue. A steer can’t overtake it and returns CONVERSATION_QUEUE_FULL.

The response also carries a Retry-After header. To follow the active execution, poll /v1/executions/{executionId}/status. To add your message to it instead, send the same message again with x-runtype-concurrency: steer. The steer’s receipt names the same executionId with initial: false.

Safe boundaries and limits

Review these constraints before accepting additional input in your application:

Input follows first-in, first-out order and can’t overtake an earlier queued request. A model call or tool batch in flight finishes first, and Runtype saves completed tool results before it adds the input.

Runtype doesn’t withdraw streamed output or roll back external side effects. Approval, client-tool, and elicitation pauses remain paused. Text can’t approve a tool or replace a structured resume response.

Steering doesn’t reset turn, time, cost, recovery, or pause-expiry limits. It doesn’t reserve a second execution. Several messages can share one answer.

If the execution is already finishing, later input starts or queues a separate execution with the usual admission rules and quotas. If a queued steer follows a failed or cancelled execution, its input becomes not_applied. If you cancel a queued execution before it starts, Runtype removes it and marks its input not_applied. It never runs later.

Input sent while the execution is starting becomes not_applied if that execution never starts, for example because another request supersedes it.

Each delivery accepts one to 16 user messages and at most 64 KB. Runtype limits how many deliveries can wait on one conversation.

A steer uses the conversation’s earlier successful history. A very long conversation can exceed the history that Runtype keeps. In that case, start a new conversation.

Steer from a browser client

Browser clients use a separate protocol: POST /v1/client/chat with submitMode: "steer", not the concurrency header. Never put a Runtype API key in browser code.

Set behavior.durableTurns.enabled to true on the chat surface and bind a saved Runtype agent. Initialize with the surface’s public client token and durableRecovery: true. Retain the visitor token returned by /v1/client/init. Then initialize again with that visitor token and session ID to claim the conversation. Only offer steering when init returns durableRecovery.steer: true. A surface with history disabled can still support durable recovery and steering.

A client token that isn’t bound to a surface carries the same opt-in on the token itself: set config.durableTurns.enabled to true. Each token kind reads exactly one opt-in. A surface-bound token always reads the surface behavior, so setting config.durableTurns.enabled on it has no effect.

Each browser send contains exactly one additional user message. Its id must equal the request’s turnId. When you retry an uncertain send, keep that ID and the entire payload unchanged. Mint a new ID for a genuinely new message. Don’t resend the transcript, alter execution options, or replace structured tool answers with chat text.

Browser admission
const turnId = crypto.randomUUID()
const response = await fetch(`${apiUrl}/v1/client/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Visitor-Token': visitorToken,
},
body: JSON.stringify({
sessionId,
turnId,
assistantMessageId: crypto.randomUUID(),
submitMode: 'steer',
messages: [{ id: turnId, role: 'user', content: 'Also compare delivery times.' }],
}),
})

Use the apiUrl, session ID, and visitor token from your client configuration and init flow. The browser supplies its Origin header, and the surface must allow that origin.

Handle both successful admission shapes:

  • 200 SSE: A new execution streams its answer. Retain both the X-Runtype-Execution-Id and X-Runtype-Delivery-Id response headers.
  • 202 JSON: Retain executionId, deliveryId, deliveryStatus, deliveryStatusUrl, and eventsUrl. When this receipt names the execution that you already follow, keep the existing stream and any open tool question. An uncertain initial send can also return this shape. If you don’t already follow that execution, attach to eventsUrl.

If steering isn’t available, the request returns 409 with code STEER_UNAVAILABLE. This happens when the surface doesn’t have durable turns enabled, the agent isn’t a saved Runtype agent that runs durably, or the conversation doesn’t belong to this visitor. Fall back to a normal send, or wait for the current reply to finish.

Read the returned delivery URL with ?sessionId=... and the same X-Visitor-Token header. Attaching to execution events uses the same credentials. To avoid displaying replayed events twice, retain the SSE cursor. Browser recovery URLs are scoped under /v1/client/conversations/{conversationId}/executions/{executionId}. A different visitor or conversation can’t use the receipt.

Disconnecting, reloading, or closing the HTTP stream does not cancel the execution. To stop it, send POST to /v1/client/conversations/{conversationId}/executions/{executionId}/cancel?sessionId=... with the visitor header. Cancellation can leave pending input not_applied. Resubmit it only when the user sends it again. A steer never cancels the execution, closes an elicitation, or consumes a tool’s pending resume response.

Troubleshoot steer errors

The following table lists common errors that a steer request can return:

CodeStatusCauseWhat to do
CONVERSATION_BUSY409A non-steer request overlapped an active execution.Wait for Retry-After, or resend with x-runtype-concurrency: steer.
CONVERSATION_QUEUE_FULL409Another request is already waiting in the queue.Wait for the queued execution to start or finish.
DELIVERY_CHECKPOINT_FULL409The execution has no room for more input.Wait for the execution to finish, or use a new conversation.
DELIVERY_IDEMPOTENCY_CONFLICT409The idempotency key already names different input.Use a new idempotency key for a new message.
DELIVERY_INBOX_FULL409Too many deliveries are waiting on this conversation.Wait for pending input to settle.
DELIVERY_TOO_LARGE413The messages exceed 64 KB.Send less content.
INVALID_CONCURRENCY_MODE400On dispatch, the x-runtype-concurrency value isn’t reject, supersede, queue, or steer.Use one of the four supported values.
INVALID_STEER_ADMISSION400The request has no conversationId, doesn’t name a saved agent, or also sets x-runtype-coalesce.Send a conversationId for a saved agent, and remove x-runtype-coalesce.
INVALID_STEER_MESSAGES400The request doesn’t contain one to 16 user messages.Send only the new user messages.
STEER_CONFIG_MISMATCH409The steer’s options, end-user identity, or API key differ from the active execution.Resend with the original values, wait for the execution to finish, or use a new conversation. Don’t drop identity fields to force a match.
STEER_HISTORY_UNAVAILABLE409The conversation’s history exceeds what Runtype keeps.Start a new conversation.
STEER_HOST_UNSUPPORTED409The active execution can’t accept additional input.Wait for the execution to finish, then send the message again.
STEER_UNSUPPORTED_LANE400The agent turn doesn’t run durably, or dispatch can’t steer this agent or request.Remove durability.forced: "in_process" and dispatch-only inputs, or use the saved-agent execute endpoint.
STORED_HISTORY_UNSUPPORTED400The request combines steer with history: "stored".Remove history: "stored".

Next steps

Use these guides to route messages, configure tools, and preserve caller scope: