Protocol lifecycle
Understand the Letta App Server WebSocket connection, runtime_start, input, sync, abort, and event handling
The Letta App Server uses one bidirectional WebSocket. Every command, response, and event is a JSON object with a type field. Commands that expect a direct response include a request_id; runtime events include sequencing and idempotency metadata.
Transport
Section titled “Transport”Start App Server:
letta server --listen ws://127.0.0.1:4500The process prints one WebSocket endpoint:
Listening on ws://127.0.0.1:4500WebSocket: ws://127.0.0.1:4500/wsOpen one connection to /ws. Send every command and receive every response, approval, tool callback, streamed delta, and state update on that socket. App Server rejects the legacy ?channel=control and ?channel=stream layout with HTTP 426.
Loopback listeners do not require authentication. A non-loopback listener refuses to start unless you configure --ws-auth capability-token or --ws-auth signed-bearer-token; native clients send the credential as Authorization: Bearer ... during the WebSocket upgrade.
App Server accepts ws:// listen URLs. Terminate TLS at a reverse proxy when clients connect over wss://. The standard browser WebSocket API cannot set the required Authorization header, so authenticated browser applications need a trusted backend or WebSocket proxy.
HTTP requests carrying an Origin header are rejected. An Origin-bearing WebSocket upgrade is accepted only when App Server authentication is configured and the native client also sends valid authorization.
The server exposes these unauthenticated, no-origin health probes:
| Endpoint | Result |
|---|---|
GET /readyz | 200 OK when the listener accepts connections |
GET /healthz | 200 OK while the process is healthy |
Capability discovery
Section titled “Capability discovery”Before relying on optional protocol features, read App Server’s version and capabilities over HTTP:
GET /app-server-infoAuthorization: Bearer <token>When authentication is enabled, this endpoint requires the same bearer token as the WebSocket. You can also request the information after connecting:
{ "type": "app_server_info", "request_id": "info-1" }The app_server_info_response includes the Letta Code version, numeric protocol version, active backend, and capability flags such as runtime_start and split_channels. Current App Server versions report split_channels: false.
Connections and runtime subscriptions
Section titled “Connections and runtime subscriptions”App Server accepts multiple concurrent WebSocket clients. Each connection has independent request correlation, runtime subscriptions, event sequencing, external tools, approvals, and terminal processes.
runtime_start subscribes the sending connection to the returned {agent_id, conversation_id} scope. One connection can subscribe to multiple runtimes, and multiple connections can subscribe to the same runtime. Runtime-scoped events are delivered only to subscribed connections; disconnecting one client does not close the others.
Runtime startup
Section titled “Runtime startup”Send runtime_start before sending turns. It resolves the agent and conversation, applies runtime state, registers external tools, and replays state.
{ "type": "runtime_start", "request_id": "runtime-1", "agent_id": "agent-123", "conversation_id": "conv-123", "cwd": "/Users/me/project", "mode": "standard", "client_info": { "name": "my_app", "title": "My App", "version": "0.1.0" }, "recover_approvals": true, "force_device_status": true}The response contains the canonical runtime scope:
{ "type": "runtime_start_response", "request_id": "runtime-1", "success": true, "runtime": { "agent_id": "agent-123", "conversation_id": "conv-123" }, "created": { "agent": false, "conversation": false }, "agent": {}, "conversation": {}}Use the returned runtime for all scoped commands. Do not reconstruct it from assumptions if App Server created either object.
Creating agents and conversations
Section titled “Creating agents and conversations”runtime_start accepts exactly one of agent_id or create_agent.
{ "type": "runtime_start", "request_id": "runtime-1", "create_agent": { "body": { "name": "Build Agent", "memory_blocks": [] }, "pin_global": true }, "create_conversation": { "body": {} }}Pass an existing conversation_id to resume that conversation. If you omit it, App Server creates a conversation for the resolved agent; create_conversation.body optionally supplies its creation fields. The special conversation ID default selects the agent’s virtual default conversation.
Runtime state fields
Section titled “Runtime state fields”| Field | Direction | Meaning |
|---|---|---|
cwd | runtime_start, change_device_state | Working directory for local tools. null resets to the listener boot directory. |
mode | runtime_start, change_device_state | Permission mode for local tool execution. |
skill_sources | runtime_start | Skill sources available to the resolved runtime. |
recover_approvals | runtime_start, sync | Probe backend state for stale pending approvals. Defaults to true. |
force_device_status | runtime_start, sync | Force a device status replay even if the cached status did not change. |
client_info | runtime_start | Client metadata for diagnostics and future negotiation. |
external_tools | runtime_start | Controller-owned tools registered for this connection and runtime. |
Valid permission modes are standard, acceptEdits, unrestricted, and strict.
Sending turns
Section titled “Sending turns”Use input with payload.kind: "create_message" to send a user turn.
{ "type": "input", "runtime": { "agent_id": "agent-123", "conversation_id": "conv-123" }, "payload": { "kind": "create_message", "messages": [ { "role": "user", "content": "Inspect the auth flow and report risks.", "client_message_id": "client-msg-1" } ], "client_tool_allowlist": ["Read", "Grep", "Glob"] }}client_message_id is optional, but useful for UI deduplication and local optimistic rows.
client_tool_allowlist narrows the locally executed client tools for this turn. Omit it to use the runtime’s normal toolset. Pass an empty array to expose no client tools for the turn.
Use external_tool_scope_ids to expose scoped controller tools registered by runtime_start. Set exclude_interactive_tools: true for headless clients that cannot surface mid-turn questions to a person.
Approval responses
Section titled “Approval responses”Use input with payload.kind: "approval_response" to answer a pending approval request.
{ "type": "input", "runtime": { "agent_id": "agent-123", "conversation_id": "conv-123" }, "payload": { "kind": "approval_response", "request_id": "approval-123", "decision": { "behavior": "allow", "message": "Approved by controller" } }}{ "type": "input", "runtime": { "agent_id": "agent-123", "conversation_id": "conv-123" }, "payload": { "kind": "approval_response", "request_id": "approval-123", "decision": { "behavior": "deny", "message": "Do not modify production files" } }}Streaming and completion
Section titled “Streaming and completion”The primary turn event is stream_delta:
{ "type": "stream_delta", "runtime": { "agent_id": "agent-123", "conversation_id": "conv-123" }, "event_seq": 42, "emitted_at": "2026-07-31T18:00:00.000Z", "idempotency_key": "stream_delta:42:550e8400-e29b-41d4-a716-446655440000", "delta": { "id": "msg-1", "date": "2026-06-17T00:00:00.000Z", "message_type": "status", "message": "Reading files", "level": "info" }}A turn is complete when a stream_delta carries delta.message_type: "stop_reason", except for stop_reason: "requires_approval". Approval is a continuation boundary: keep the runtime active, render the matching control_request, and send an approval response. Treat loop_error and error_message deltas as failures.
Also handle runtime and controller events:
| Event | Use |
|---|---|
control_request | Pending permission request |
external_tool_call_request | Controller-owned tool callback |
update_loop_status | Active run IDs and waiting/running state |
update_device_status | Runtime availability, cwd, mode, and status snapshots |
update_queue | Full turn queue snapshot |
update_subagent_state | Subagent state snapshots |
stream_delta | Agent output and lifecycle deltas |
All events arrive on the same WebSocket. Route scoped events by runtime, correlate direct responses by request_id, and process increasing event_seq values per connection when present. Use idempotency_key to deduplicate replayed or retried events. Preserve unknown fields and event types so clients remain forward-compatible.
A successful runtime_start automatically replays the subscribed runtime’s current state after its response. Use sync on an established connection when a UI needs another replay or explicit pending-approval recovery.
{ "type": "sync", "request_id": "sync-1", "runtime": { "agent_id": "agent-123", "conversation_id": "conv-123" }, "recover_approvals": false, "force_device_status": true}The replayed state arrives as normal events. App Server sends sync_response after the replay succeeds or returns an error if the replay fails.
{ "type": "sync_response", "request_id": "sync-1", "runtime": { "agent_id": "agent-123", "conversation_id": "conv-123" }, "success": true}After transport loss, open a new WebSocket and send runtime_start again to restore subscriptions and connection-owned external tools. Disconnect cleanup removes that connection’s pending approvals, tool callbacks, terminals, and queued input. If no other subscribed client can take over an active runtime, App Server requests cancellation of its active turn.
Use abort_message to stop active work or interrupt a pending approval.
{ "type": "abort_message", "request_id": "abort-1", "runtime": { "agent_id": "agent-123", "conversation_id": "conv-123" }}{ "type": "abort_message_response", "request_id": "abort-1", "runtime": { "agent_id": "agent-123", "conversation_id": "conv-123" }, "aborted": true, "success": true}Management and computer commands
Section titled “Management and computer commands”App Server exposes the same management and computer capabilities used by Letta’s own clients. Current command groups include:
- App Server capability discovery
- Agent and conversation creation, retrieval, updates, deletion, forking, message listing, and compaction
- Filesystem, content search, and git branch operations
- Memory files, history, commit diffs, and MemFS enablement
- Models, providers, toolsets, experiments, and reflection settings
- Skills and secrets
- Terminals and command execution
- Schedules and run history
- Channels, accounts, routes, targets, and pairings
Use request_id on commands that return a response. TypeScript clients can import the current WsProtocolCommand and WsProtocolMessage unions from @letta-ai/letta-code/app-server-protocol instead of copying a command list. Keep handlers tolerant of additional fields and unknown message types.
Legacy command names to avoid
Section titled “Legacy command names to avoid”Do not use these legacy listener command names in new App Server clients:
| Do not send | Use instead |
|---|---|
request_state | sync |
change_cwd | change_device_state with payload.cwd |
change_mode | change_device_state with payload.mode |
cancel_run | abort_message |
recover_pending_approvals | sync with recover_approvals: true |