Join an active agent execution

Use x-runtype-concurrency: join to send another message while your saved native Runtype agent is working. A compatible active conversation accepts that input into the same execution. An idle conversation 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.
joinAdds input to the active execution.Send additional information.

Join is opt-in. The default remains reject. Join requires a conversationId and a supported native Runtype agent. Use POST /v1/dispatch or POST /v1/agents/{agentId}/execute. Unsupported agents return an explicit error. For client tools, use the saved-agent execute endpoint because dispatch doesn’t support client tools.

Send only additional user messages

Keep the same agent, conversation, tenant/end-user scope, credential authority, and execution options on every send. Send only additional user messages, not the full transcript or replacement system instructions. Don’t combine join with x-runtype-coalesce.

Set RUNTYPE_API_KEY to your Runtype API key. Use a different idempotency key for each logical message. To send additional input asynchronously with the TypeScript SDK, use this example:

TypeScript SDK
1import { RuntypeClient } from '@runtypelabs/sdk'
2
3const client = new RuntypeClient({ apiKey: process.env.RUNTYPE_API_KEY! })
4const receipt = await client.dispatch.executeAsync(
5 {
6 agent: { agentId: 'agent_YOUR_AGENT_ID' },
7 conversationId: 'conversation-123',
8 messages: [{ role: 'user', content: 'Also compare the delivery times.' }],
9 options: { streamResponse: false },
10 },
11 { concurrency: 'join', idempotencyKey: 'user-message-42' }
12)
13console.log(receipt.executionId, receipt.deliveryId)

Replace agent_YOUR_AGENT_ID with your saved native agent’s ID.

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

Python SDK
1import os
2from runtype import RuntypeClient
3
4with RuntypeClient(
5 api_key=os.environ["RUNTYPE_API_KEY"],
6 headers={
7 "Prefer": "respond-async",
8 "x-runtype-concurrency": "join",
9 "Idempotency-Key": "user-message-42",
10 },
11) as client:
12 receipt = client.dispatch(
13 {
14 "agent": {"agentId": "agent_YOUR_AGENT_ID"},
15 "conversationId": "conversation-123",
16 "messages": [
17 {"role": "user", "content": "Also compare the delivery times."}
18 ],
19 },
20 stream=False,
21 )
22 print(receipt["executionId"], receipt["deliveryId"])

Replace agent_YOUR_AGENT_ID with your saved native agent’s ID.

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: join' \
> -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 }
> }'

Replace agent_YOUR_AGENT_ID with your saved native agent’s ID.

The TypeScript SDK also accepts admission options on the client.agents.execute method. Streaming dispatch accepts those options in its request initialization argument.

Track the delivery, not a separate execution

An asynchronous response includes executionId, deliveryId, deliveryStatus, and deliveryStatusUrl. Multiple deliveries can name the same execution. Poll the returned delivery URL with the same authorization. With the TypeScript SDK, you can use the client.executions.getDelivery(executionId, deliveryId) method.

Use the delivery state to distinguish acceptance from application:

  • pending: Runtype stored the input but hasn’t incorporated it into the execution’s checkpoint.
  • applied: Runtype durably incorporated the input into the execution’s context. This doesn’t promise that the model follows the message.
  • settled: Applied input shares the execution’s terminal outcome, including failure or cancellation.
  • not_applied: The execution ended before using the input. Runtype doesn’t restart it automatically.

Retry the same message with the same idempotency key to recover its delivery receipt. If you reuse that key with different messages or execution options, Runtype returns DELIVERY_IDEMPOTENCY_CONFLICT. Receipt retention is bounded; receipts aren’t a permanent transcript archive.

Without Prefer: respond-async, blocking and server-sent events (SSE) callers follow the execution’s answer. SSE carries X-Runtype-Delivery-Id and replays the execution’s event stream. Merge events by execution identity and event cursor to avoid displaying the same answer twice.

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 queue-only request. A model call or tool batch in flight finishes first. Runtype checkpoints completed tool results before applying additional 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.

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

After completion seals the active inbox, later input starts or queues a separate execution with ordinary admission and quotas. If a queued join follows a failed or cancelled execution, its input becomes not_applied.

Each delivery accepts one to 16 user messages and at most 64 KB. Runtype bounds pending deliveries and retained receipts. If the inbox is full, Runtype returns DELIVERY_INBOX_FULL.

DELIVERY_CHECKPOINT_FULL refuses a send that exceeds the execution’s checkpoint capacity, including pending input. This refusal doesn’t interrupt the execution. Later model or tool output can still exhaust storage limits.

An execution that exceeds the checkpoint size limit can refuse joins with JOIN_HOST_UNSUPPORTED. Accepted input requires durable checkpointing.

JOIN_CONFIG_MISMATCH means the active execution has different options, data scope, or credential authority. Don’t retry by dropping security context. Wait for the execution to finish or use a different conversation.

Later delta-only executions reuse retained successful conversation history. If Runtype can’t use that history safely, JOIN_HISTORY_UNAVAILABLE requires a different conversation.

Join from a browser client

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

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

Each browser send contains exactly one additional user message. Its id must equal the request’s turnId. Keep that ID and the entire payload unchanged when retrying an uncertain send; 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
1const turnId = crypto.randomUUID()
2const response = await fetch(`${apiUrl}/v1/client/chat`, {
3 method: 'POST',
4 headers: {
5 'Content-Type': 'application/json',
6 'X-Visitor-Token': visitorToken,
7 },
8 body: JSON.stringify({
9 sessionId,
10 turnId,
11 assistantMessageId: crypto.randomUUID(),
12 submitMode: 'join',
13 messages: [{ id: turnId, role: 'user', content: 'Also compare delivery times.' }],
14 }),
15})

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

Handle both successful admission shapes:

  • 200 SSE: A new host streams its answer. Retain both X-Runtype-Execution-Id and X-Runtype-Delivery-Id response headers.
  • 202 JSON: Retain executionId, deliveryId, deliveryStatus, deliveryStatusUrl, and eventsUrl. Keep an existing stream and any open tool question intact when this receipt names the same host. An uncertain initial start can also return this recovery handle; attach to eventsUrl when you don’t already follow that execution.

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

Disconnecting, reloading, or aborting the HTTP stream does not cancel the execution. An explicit Stop action sends POST to /v1/client/conversations/{conversationId}/executions/{executionId}/cancel?sessionId=... with the visitor header. Cancellation can leave pending inputs not_applied; only an explicit new send should resubmit them. Joining must not cancel the host, close an elicitation, or consume a tool’s pending resume response.

Next steps

Use these guides to configure tools and preserve caller scope: