Runtime Server
The runtime executes agents and exposes chat, session, and tool APIs. Hosted deployments supply authentication and durable storage through the platform. The CLI runs a local server with in-memory storage.
Use the Platform API to manage agents, source, and deployments. Use the endpoints below to interact with a running agent.
Endpoints
Chat
| Method | Path | Response |
|---|---|---|
POST | /chat or /chat/stream | SSE stream of Amodal events |
POST | /chat/sync | Complete response after execution finishes |
POST | /chat/ai-stream | AI SDK streaming protocol |
The ordinary chat SSE stream is a POST response. Use fetch or the SDK; browser EventSource supports GET only.
Runtime auth from curl
Hosted chat requires a runtime bearer token or the hosted browser session flow. Public access to an app's static files does not grant access to chat.
With AMODAL_API_BASE, AMODAL_AGENT_ID, AMODAL_PLATFORM_TOKEN, and AMODAL_AGENT_URL set, mint a short-lived runtime token:
AMODAL_AGENT_TOKEN="$(
curl -fsS "$AMODAL_API_BASE/api/agents/$AMODAL_AGENT_ID/tokens" \
-H "Authorization: Bearer $AMODAL_PLATFORM_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ttl":600}' \
| node -e 'const fs = require("fs"); const body = JSON.parse(fs.readFileSync(0, "utf8")); process.stdout.write(body.token)'
)"
curl -fN "$AMODAL_AGENT_URL/chat/stream" \
-H "Authorization: Bearer $AMODAL_AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Say hello."}'The platform token needs permission to mint runtime tokens for that agent. Keep it on your backend. See Authentication for persistent agent API keys and browser authentication.
Chat request body
| Field | Type | Description |
|---|---|---|
message | string, required | User text. May be empty when images or attachments are supplied. |
session_id | string | Continue this conversation; omit to start one. |
scope_id | string | Tenant, user, or object scope. Scope requirements depend on the host; see below. |
context | Record<string, string> | App facts for prompt templates and connection context injection. These are not trusted authorization claims. |
metadata | Record<string, string> | Observability only: recorded on the turn's trace and the session, never sent to the model. See below. |
agent | string | Named agent under agents/. Omit to use default when defined, otherwise the unscoped default chat. |
skill | string | Select a skill-scoped session. |
deploy_id | string | Deployment hint for a host that resolves bundles dynamically. It does not switch the deployment behind a fixed agent URL. |
model | {provider, model, effort?} | Model selection for the session, subject to the host's provider access. |
max_session_tokens | positive integer | Cumulative session token budget. Checked between transitions, so usage can exceed it during a model call. |
images | array | Up to 5 image attachments. |
attachments | array | Files as {mimeType, data, filename?, delivery?}. data is base64 without a data-URI prefix. |
A scope can contain several sessions. Sending the same scope_id without session_id starts another conversation. Your application backend must authorize the scope and any requested session before forwarding the call; see Embedding.
The scope.requireScope check applies to ordinary chat routes with a static bundle, including local development. Hosted chat resolves bundles dynamically and does not apply that check. For hosted multi-tenant use, proxy calls through your backend, authorize each scope and session, and retain runtime credentials there. A scoped runtime token alone does not isolate session access.
Attachments support PDF, plain text, Markdown, CSV, PNG, JPEG, GIF, WebP, Office Open XML, OpenDocument, and RTF. Office, OpenDocument, and RTF files are converted to text at ingestion. Each PNG, JPEG, GIF, or WebP image embedded in an Office or OpenDocument file follows its text as a separate image attachment named <filename>/<image>, up to 20 images and 15 MB across all attachments in the request, each image at most 3.75 MiB. The converted text lists the attached images and any embedded media left out, with the reason.
metadata describes where the request came from, such as {"userId": "u_42", "screen": "/billing"}. It accepts at most 50 keys, each up to 64 characters, with string values up to 1,024 characters; a request over these limits is rejected with 400. The runtime records each key on the turn's root span as amodal.meta.<key> and keeps the latest value of each key on the session. The model, tools, prompt templates, and guardrails never see it. Any caller can set any value, so Amodal labels it unverified. The authenticated user, organization, and sign-in method are recorded separately as amodal.user.id, amodal.user.org_id, and amodal.auth.method, and metadata cannot override them. Amodal shows the email of an authenticated user who is an active member with read access to the agent's organization or an active collaborator on the agent. Amodal filters runs, sessions, and feedback by the userId and screen keys. Do not put secrets or sensitive personal data in metadata.
The per-file base64 limit is 28,000,000 characters (about 20 MB decoded), within a 50 MB JSON request limit. There is no separate attachment-count limit. delivery: "inline" sends content to the model; "context" exposes a manifest to the model and keeps the bytes available to tools. When omitted, runtime delivery policy applies.
Sessions
| Method | Path | Purpose |
|---|---|---|
POST | /sessions | Prepare a session before its first message. |
GET | /sessions/history | List sessions, up to 500; accepts ?scope_id=. |
GET | /sessions/history/:id | Read a session's history. |
PATCH | /sessions/history/:id | Update session metadata. |
DELETE | /sessions/history/:id | Delete a session. |
On a hosted runtime, these routes require authentication. History and pending-approval lists hide sessions owned by another verified user. Reading, updating, or deleting another user's session returns 404, the same response as a missing session. Updates and deletions check both live and persisted ownership. Sessions without an owner remain shared, and trusted server credentials without a verified user ID retain access to owned sessions.
scope_id is a caller-supplied list filter, not an ownership credential. Lists filter ownership after fetching up to 500 rows, so fewer visible sessions can be returned even when other accessible sessions exist. For tenant-specific rules or calls using server credentials, have your application backend authorize each operation and retain the runtime credential.
Interactions
| Method | Path | Purpose |
|---|---|---|
POST | /chat/sessions/:id/ask-user-response | Answer an ordinary chat confirmation with {correlation_id, approved}. ask_id is also accepted. |
POST | /api/sessions/:id/answer | Answer a parked durable workflow. |
Use the correlation_id from confirmation_required for an ordinary confirmation. The chat loop waits for that response. Choice selections from ask_choice are sent as the next chat message.
A durable_approval_required event identifies a separate parked session. Answer that session through the durable workflow API; see Agent Workflows.
System
| Method | Path | Purpose |
|---|---|---|
GET | /health | Health check |
GET | /api/events | Runtime state subscription, when mounted by the host |
POST | /channels/:channelType/webhook | Inbound messaging-channel webhook |
SSE event types
Chat frames contain a JSON object with type, timestamp, and event-specific fields. The server sends data: frames without a separate event: line.
Common events are listed below. The React SDK exports SSEEvent from @amodalai/react for typed chat stream handling.
| Type | Fields | Purpose |
|---|---|---|
init | session_id | Session created or resumed |
text_delta | content | Incremental answer text |
thinking_delta | content | Model reasoning output, when supplied |
tool_call_start | tool_name, tool_id, parameters | Tool execution started |
tool_call_result | tool_id, status, result?, content? | Tool result or error |
tool_log | tool_name, message | Handler progress log |
subagent_event | parent_tool_id, agent_name, event_type | Specialist activity |
skill_activated | skill_name | Skill loaded |
widget | widget_type, data | Structured content for the client |
confirmation_required | endpoint, method, reason, escalated, correlation_id? | Ordinary tool approval |
durable_approval_required | session_id, tool_name, question? | Parked workflow approval |
agent_handoff | from, to, removed_tools | Session changed its root agent |
compaction_start | estimated_tokens, threshold | Context summarization started |
compaction_end | tokens_before, tokens_after | Context summarization completed |
error | message | Execution error |
done | reason?, turns?, trace_id?, usage? | Turn completed |
Streaming example
data: {"type":"init","session_id":"session-123","timestamp":"2026-09-07T10:00:00Z"}
data: {"type":"text_delta","content":"Hello.","timestamp":"2026-09-07T10:00:01Z"}
data: {"type":"done","reason":"model_stop","turns":1,"usage":{"input_tokens":100,"output_tokens":2,"cached_tokens":0,"total_tokens":102},"timestamp":"2026-09-07T10:00:01Z"}trace_id is the trace of the run that produced the answer, present when the runtime exports traces. The chat widget sends it with a thumbs up or down, so the feedback links to that run in Amodal.
Building a client
streamChat parses the chat stream and accepts an abort signal and runtime token. This helper requires the React package and its peer dependencies; it can be used without rendering a chat component.
import { streamChat } from '@amodalai/react';
async function chat(runtimeUrl: string, token: string, message: string, sessionId?: string) {
for await (const event of streamChat(
runtimeUrl,
{
message,
...(sessionId ? { session_id: sessionId } : {}),
},
undefined,
token,
)) {
if (event.type === 'init') sessionId = event.session_id;
if (event.type === 'text_delta') console.log(event.content);
if (event.type === 'error') throw new Error(event.message);
}
return sessionId;
}Store the returned session ID and pass it with the next message. This minimal example prints text; it does not render widgets or answer confirmations. Use useChat or AmodalChat for those interactions.
Session lifecycle
A request without session_id creates a session. Later requests with that ID resume the conversation. The runtime records the verified user ID of the session's creator and refuses to reuse the session for a request whose verified user ID differs, starting a fresh session instead. A caller with no verified user ID, such as an API key or a service token, can resume any session. The runtime does not compare a session's scope with the caller's token scope. Your application backend must authorize a resume request before forwarding it.
POST /sessions creates a session ahead of the first message. It accepts the chat body's agent, skill, deploy_id, max_session_tokens, model, scope_id, and context, builds the session from them, and returns 201 with {session_id}. The first message that carries that ID skips the session setup and goes straight to the model. Send the same fields you will chat with, because a live session keeps its skill, deploy, model, and token budget. A prepared session is not persisted until its first turn, and an unused one expires with the idle timeout. The React SDK's chat hooks prepare a session when they mount; see Chat hooks.
The session manager evicts idle sessions from its in-memory cache after 30 minutes by default. Eviction does not delete persisted history. A configured session store can reload the conversation on the next request.
Hosted sessions use platform-backed persistence. Local development uses in-memory sessions and stores, which are lost on restart. In-process integrations choose their own storage implementations. A shared database alone does not coordinate simultaneous execution across replicas.
An ordinary chat stream disconnect cancels the active request. Sending the same session_id continues the conversation; it does not replay the interrupted SSE response. Use durable workflows when work must continue independently of the connection.
Context compaction
The default compaction threshold is 90% of the model's estimated context window. The runtime summarizes older messages and keeps the latest 6 user turns. Compaction can lose exact details; save important facts in stores when they need to remain queryable.
See Context Management for tool-result clearing, summaries, and failure handling.
Configuration
amodal.json defines agent content and optional runtime features. Authentication, model selection, persistence, and server settings are supplied by the host.
amodal dev defaults to port 3847 and accepts --port. Programmatic hosts configure createServer() or createLocalServer(), including sessionTtlMs for cache expiry.
AUTH_TOKEN provides simple bearer authentication for protected chat, session, tool, and automation routes when the host has not injected authentication middleware. It does not protect every route. Use Authentication for the hosted deployment contract.