Runtime Server
The runtime (@amodalai/runtime) is an HTTP server that hosts the agent engine and exposes it over SSE for chat clients. It handles session management, webhook ingestion, and tool execution. It is the bridge between "someone sends a message" and "the agent reasons about it and responds."
The underlying engine is a state machine — see State Machine for the architecture.
The runtime is deliberately stateless in its HTTP layer — all persistent state lives in the Postgres store backend. This means you can run multiple runtime instances behind a load balancer, and sessions will work correctly as long as they share the same database.
Endpoints
Chat
| Method | Path | Description |
|---|---|---|
POST | /chat/sync | Send a message, get a complete response (non-streaming) |
POST | /chat or /chat/stream | Send a message, stream response via SSE (both paths accepted) |
POST | /chat/ai-stream | Direct AI streaming endpoint (Vercel AI SDK protocol) |
The /chat/stream endpoint is what most clients use. You POST a message and keep the connection open — the response streams back as SSE events. The non-streaming /chat/sync endpoint waits for the full response before returning, which is simpler but means no real-time feedback during tool execution.
Runtime Auth From Curl
Runtime chat endpoints require bearer auth. Public app access can expose a static/custom SPA, but it does not authorize token-spending chat calls.
CLI and coding-agent clients should mint a short-lived agent runtime token through the Platform API using an authenticated platform automation token or user session, then call the deployed runtime with that agent 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 -N "$AMODAL_AGENT_URL/chat/stream" \
-H "Authorization: Bearer $AMODAL_AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"Say hello."}'Do not call /auth/token directly from curl; it is part of the browser OIDC handoff and returns 404 when end-user OIDC is not configured.
Chat Request Body
| Field | Type | Required | Description |
|---|---|---|---|
message | string | yes | The user message |
session_id | string | no | Session ID to continue a conversation |
scope_id | string | no | Scope ID for per-user isolation (see Scope) |
context | Record<string, string> | no | Key-value pairs associated with the scope (forwarded via contextInjection) |
images | array | no | Image attachments (max 5) |
session_type | "chat" | "admin" | "automation" | no | Controls which tools/skills load |
deploy_id | string | no | Load a specific snapshot instead of the active one |
max_session_tokens | number | no | Session-wide token budget cap |
model | {provider, model} | no | Pin this session to a specific provider/model |
Sessions
| Method | Path | Description |
|---|---|---|
GET | /sessions/history | List sessions (supports pagination) |
GET | /sessions/history/:id | Get session history |
PATCH | /sessions/history/:id | Update session metadata |
DELETE | /sessions/history/:id | Delete a session |
Interactions
| Method | Path | Description |
|---|---|---|
POST | /ask-user-response | Respond to a confirmation prompt |
POST | /widget-actions | Handle widget button/action clicks |
When the agent needs confirmation for a write operation, it emits a confirmation_required SSE event. The client shows the confirmation UI and sends the user's response back via /ask-user-response. The agent's state machine is paused in the confirming state while waiting.
System
| Method | Path | Description |
|---|---|---|
GET | /health | Health check |
POST | /webhooks | Incoming platform-managed webhooks |
SSE Event Types
When streaming via /chat/stream, the server emits these event types:
| Event | Payload | Description |
|---|---|---|
init | { sessionId } | Session created or resumed |
text_delta | { delta } | Incremental text output from the LLM |
tool_call_start | { name, id } | Tool execution beginning |
tool_call_result | { id, result } | Tool execution complete |
subagent_event | { agentId, event } | Task agent activity (dispatched work) |
skill_activated | { skill } | Skill matched and loaded into context |
widget | { type, data } | Widget rendered inline (entity-card, data-table, etc.) |
kb_proposal | { proposal } | Knowledge base update proposed |
confirmation_required | { action, details } | Waiting for user approval of a write |
approved | { action } | User approved an action |
credential_saved | { connection } | Connection credentials captured |
error | { message } | Error occurred |
done | {} | Response complete |
Streaming Example
Here is what an SSE stream looks like when a user asks a support agent to summarize a ticket:
event: init
data: {"sessionId":"1f7a7db8-593f-4a56-85bb-6ef80ad3a3a4"}
event: text_delta
data: {"delta":"I'll check the ticket details and summarize the current state."}
event: text_delta
data: {"delta":" One moment."}
event: tool_call_start
data: {"name":"request","id":"tc_01","args":{"method":"GET","path":"/tickets/T-100"}}
event: subagent_event
data: {"id":"tc_01","result":"[200 OK] {\"id\":\"T-100\",\"status\":\"open\",\"category\":\"account_access\",\"summary\":\"Customer cannot complete password reset\"}"}
event: text_delta
data: {"delta":"Ticket T-100 is open and categorized as account access. The customer cannot complete password reset."}
event: widget
data: {"type":"entity-card","data":{"title":"Ticket T-100","fields":{"Status":"Open","Category":"Account access","Next step":"Verify identity before account changes"}}}
event: text_delta
data: {"delta":" The support operator should verify identity, then retry the reset flow or escalate if verification fails."}
event: done
data: {}Notice the sequence: the agent starts with a text message explaining what it will do, calls a configured tool, renders a widget with structured data, and wraps up with a concise recommendation. The client receives all of this in real time.
Building a Client
Here is a minimal JavaScript example that consumes the SSE stream:
async function chat(message, sessionId) {
const response = await fetch("https://your-agent.example.com/chat/stream", {
method: "POST",
headers: {
"Authorization": `Bearer ${agentToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ message, session_id: sessionId }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let currentSessionId = sessionId;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop(); // keep incomplete line in buffer
let eventType = null;
for (const line of lines) {
if (line.startsWith("event: ")) {
eventType = line.slice(7);
} else if (line.startsWith("data: ") && eventType) {
const data = JSON.parse(line.slice(6));
switch (eventType) {
case "init":
currentSessionId = data.sessionId;
break;
case "text_delta":
process.stdout.write(data.delta);
break;
case "tool_call_start":
console.log(`\n[Calling ${data.name}...]`);
break;
case "tool_call_result":
console.log(`[Result received]`);
break;
case "widget":
renderWidget(data.type, data.data); // your widget renderer
break;
case "error":
console.error(`Error: ${data.message}`);
break;
case "done":
console.log("\n");
break;
}
eventType = null;
}
}
}
return currentSessionId;
}
// Usage: maintain session across messages
let sessionId = null;
sessionId = await chat("What were our top customers last month?", sessionId);
sessionId = await chat("How does that compare to the month before?", sessionId);For production use, the @amodalai/react package provides hooks that handle all of this — reconnection, event parsing, widget rendering, confirmation flows — so you do not need to build the SSE client from scratch:
useChat— full-featured hook for the main chat endpoint (/chat/stream). Includes session resume, history loading, auth tokens, and ask-user flows.useAmodalChat— convenience wrapper for apps using theAmodalProvidercontext (usesAmodalClientfor transport).useChatStream— low-level hook for custom endpoints. You supply astreamFnthat connects to any SSE endpoint; the hook handles the reducer, event loop, tool-call tracking, and widget event bus.
Session Lifecycle
Creation
The first message to /chat/stream without a session_id creates a new session. The runtime generates a unique ID, initializes the message history, compiles the initial context, and emits the init event with the session ID. The client stores this ID and sends it with all subsequent messages.
Active State
During an active session, messages accumulate and context grows. Each exchange adds the user's message, the agent's reasoning, tool calls, and tool results to the conversation history. The session is persisted to the store after each exchange.
Context Compaction
As the conversation grows, it eventually approaches the model's context window limit. When the compiled prompt exceeds 80% of the window, the context compiler triggers compaction:
- Identify compactable segments: Older conversation turns that are not part of an active tool chain
- Summarize: The runtime sends the old turns to the simple model with a summarization prompt: "Compress this conversation history into a concise summary preserving key facts, decisions, and open threads"
- Replace: The old turns are replaced with the summary in the session history
- Continue: The next LLM call uses the compacted history, freeing up context for new turns
Before compaction (context at 85%):
[system prompt] [KB index] [tools]
[turn 1: user question + agent response + 3 tool calls]
[turn 2: user question + agent response + 5 tool calls]
[turn 3: user question + agent response + 2 tool calls] ← compactable
[turn 4: user question + agent response] ← recent, kept
[turn 5: current message] ← current
After compaction (context at 52%):
[system prompt] [KB index] [tools]
[summary: "User asked about revenue trends. Agent found Q1 was up 12%.
User then asked about churn — agent identified 3 at-risk accounts.
User requested a comparison with last quarter."]
[turn 4: user question + agent response] ← kept in full
[turn 5: current message] ← currentCompaction is transparent to the user. They do not see it happen. The agent retains the key facts from earlier turns and can reference them — it just loses the exact wording and tool call details of older exchanges. For most conversations, this works well. For very long investigations where exact details from early turns matter, write those findings to a store so they survive compaction and can be queried on demand from later turns.
Idle and Expiry
Sessions that receive no messages for the configured TTL period (AMODAL_SESSION_TTL, default 3600 seconds) transition to idle. After another TTL period, idle sessions expire and their context is discarded. The session metadata (ID, creation time, message count) is retained for audit purposes, but the full conversation history is cleaned up.
Sessions persist across reconnects. If the client's SSE connection drops (network hiccup, browser tab backgrounded), the client can reconnect and resume by sending the same sessionId. The session state lives in the store, not in the SSE connection.
Background Runs
Scheduled and webhook-triggered background runs are platform-managed. The runtime can execute a background session when the hosting layer provides the binding, prompt, intent target, and delivery context.
Use Amodal or the Platform API to inspect background run status, logs, and resulting sessions.
Configuration
The runtime reads from amodal.json and environment variables:
Environment Variables
| Variable | Default | Description |
|---|---|---|
PORT | 3847 | Server port |
AMODAL_SESSION_TTL | 3600 | Session timeout in seconds |
AUTH_TOKEN | — | When set, the runtime requires Authorization: Bearer <token> on all requests. Simple bearer token auth for non-JWT deployments. |
Store Backend
The runtime requires a PostgreSQL database for session and store data. Set DATABASE_URL in your .env file or hosting environment.
Postgres enables multiple runtime instances to share session state (required for load balancing) and provides proper backup and recovery.
Cloud Runtime
In Amodal Cloud, Amodal and the Platform API create and manage runtime deployments. Operators normally do not start runtime processes manually. Use the Deploys page to see the active deployment, runtime state, production URL, and promotion history.