Skip to content
Letta Code Letta Code Letta Docs
Sign up

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.

Start App Server:

Terminal window
letta app-server --listen ws://127.0.0.1:4500

Open two websocket connections:

ChannelURLUse
Controlws://127.0.0.1:4500/ws?channel=controlCommands, request responses, external tool callbacks
Streamws://127.0.0.1:4500/ws?channel=streamStreamed turn output and runtime updates

The server also exposes health probes:

EndpointResult
GET /readyz200 OK when the listener accepts connections
GET /healthz200 OK for no-origin local health checks

Send runtime_start before sending turns. It resolves the agent and conversation, applies runtime state, registers external tools, and replays state.

runtime_start
{
"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:

runtime_start_response
{
"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.

runtime_start accepts exactly one of agent_id or create_agent.

Create an 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.

FieldDirectionMeaning
cwdruntime_start, change_device_stateWorking directory for local tools. null resets to the listener boot directory.
moderuntime_start, change_device_statePermission mode for local tool execution.
recover_approvalsruntime_start, syncProbe backend state for stale pending approvals.
force_device_statusruntime_start, syncForce a device status replay even if the cached status did not change.
client_inforuntime_startClient metadata for diagnostics and future negotiation.

Valid permission modes match Letta Code device permission modes, including standard, acceptEdits, memory, and unrestricted.

Use input with payload.kind: "create_message" to send a user turn.

input create_message
{
"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 input with payload.kind: "approval_response" to answer a pending approval request.

Allow a tool call
{
"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"
}
}
}
Deny a tool call
{
"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"
}
}
}

The primary turn event is stream_delta:

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:

EventUse
update_loop_statusActive run IDs and waiting/running state
update_device_statusRuntime availability, cwd, mode, and status snapshots
update_queueFull turn queue snapshot
update_subagent_stateSubagent state snapshots
stream_deltaAgent 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.

sync
{
"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.

sync_response
{
"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.

abort_message
{
"type": "abort_message",
"request_id": "abort-1",
"runtime": {
"agent_id": "agent-123",
"conversation_id": "conv-123"
}
}
abort_message_response
{
"type": "abort_message_response",
"request_id": "abort-1",
"runtime": {
"agent_id": "agent-123",
"conversation_id": "conv-123"
},
"aborted": true,
"success": true
}

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.

Do not use v1 listener command names in new App Server clients:

Do not sendUse instead
request_statesync
change_cwdchange_device_state with payload.cwd
change_modechange_device_state with payload.mode
cancel_runabort_message
recover_pending_approvalssync with recover_approvals: true