Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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

MethodPathDescription
POST/chat/syncSend a message, get a complete response (non-streaming)
POST/chat or /chat/streamSend a message, stream response via SSE (both paths accepted)
POST/chat/ai-streamDirect 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

FieldTypeRequiredDescription
messagestringyesThe user message
session_idstringnoSession ID to continue a conversation
scope_idstringnoScope ID for per-user isolation (see Scope)
contextRecord<string, string>noKey-value pairs associated with the scope (forwarded via contextInjection)
imagesarraynoImage attachments (max 5)
session_type"chat" | "admin" | "automation"noControls which tools/skills load
deploy_idstringnoLoad a specific snapshot instead of the active one
max_session_tokensnumbernoSession-wide token budget cap
model{provider, model}noPin this session to a specific provider/model

Sessions

MethodPathDescription
GET/sessions/historyList sessions (supports pagination)
GET/sessions/history/:idGet session history
PATCH/sessions/history/:idUpdate session metadata
DELETE/sessions/history/:idDelete a session

Interactions

MethodPathDescription
POST/ask-user-responseRespond to a confirmation prompt
POST/widget-actionsHandle 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

MethodPathDescription
GET/healthHealth check
POST/webhooksIncoming platform-managed webhooks

SSE Event Types

When streaming via /chat/stream, the server emits these event types:

EventPayloadDescription
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 the AmodalProvider context (uses AmodalClient for transport).
  • useChatStream — low-level hook for custom endpoints. You supply a streamFn that 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:

  1. Identify compactable segments: Older conversation turns that are not part of an active tool chain
  2. 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"
  3. Replace: The old turns are replaced with the summary in the session history
  4. 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]                                  ← current

Compaction 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

VariableDefaultDescription
PORT3847Server port
AMODAL_SESSION_TTL3600Session timeout in seconds
AUTH_TOKENWhen 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.