Protocol lifecycle
Understand App Server websocket channels, runtime_start, input, sync, abort, and event handling
App Server uses native v2 websocket frames. Every command and event is a JSON object with a type field. Commands are sent on the control channel. Events can arrive on either channel.
Transport
Section titled “Transport”Start App Server:
letta app-server --listen ws://127.0.0.1:4500Open two websocket connections:
| Channel | URL | Use |
|---|---|---|
| Control | ws://127.0.0.1:4500/ws?channel=control | Commands, request responses, external tool callbacks |
| Stream | ws://127.0.0.1:4500/ws?channel=stream | Streamed turn output and runtime updates |
The server also exposes health probes:
| Endpoint | Result |
|---|---|
GET /readyz | 200 OK when the listener accepts connections |
GET /healthz | 200 OK for no-origin local health checks |
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": {} }}It accepts either an existing conversation_id or create_conversation. If you pass create_agent, you should also pass create_conversation for the first runtime.
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. |
recover_approvals | runtime_start, sync | Probe backend state for stale pending approvals. |
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. |
Valid permission modes match Letta Code device permission modes, including standard, acceptEdits, memory, and unrestricted.
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.
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" }, "delta": { "id": "msg-1", "date": "2026-06-17T00:00:00.000Z", "message_type": "status", "message": "Reading files", "level": "info" }}A turn is normally complete when the stream emits a stream_delta whose delta.message_type is stop_reason. Treat loop_error and error_message deltas as failures.
Also listen for runtime updates:
| Event | Use |
|---|---|
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 |
Do not assume every event for a turn arrives on the stream channel. Subscribe to both channels.
Use sync to replay current state after reconnects or UI reloads.
{ "type": "sync", "request_id": "sync-1", "runtime": { "agent_id": "agent-123", "conversation_id": "conv-123" }, "recover_approvals": false, "force_device_status": true}The response only acknowledges the sync request. The replayed state arrives as normal events.
{ "type": "sync_response", "request_id": "sync-1", "runtime": { "agent_id": "agent-123", "conversation_id": "conv-123" }, "success": true}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}Device capability commands
Section titled “Device capability commands”App Server also forwards device capability commands through the same v2 transport. Use these when building richer UIs around the runtime:
- Filesystem:
list_in_directory,get_tree,read_file,write_file,edit_file,file_ops,watch_file,unwatch_file - Search:
search_files,grep_in_files - Memory:
list_memory,read_memory_file,write_memory_file,delete_memory_file,memory_history,memory_commit_diff,memory_file_at_ref - Models and providers:
list_models,update_model,update_toolset,list_connect_providers,connect_provider,disconnect_provider - Terminals:
terminal_spawn,terminal_input,terminal_resize,terminal_kill - Schedules:
cron_list,cron_add,cron_get,cron_update,cron_delete,cron_delete_all,cron_runs,cron_trigger - Channels:
channels_list,channel_accounts_list,channel_account_create, and related channel management commands
Use request_id on commands that return a response. Keep command handlers tolerant of additional fields.
Legacy command names to avoid
Section titled “Legacy command names to avoid”Do not use v1 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 |