Quickstart
Start Letta Code App Server, connect websocket channels, and run a turn
Start App Server when your application needs protocol-level access to a local Letta Code runtime.
Install Letta Code
Section titled “Install Letta Code”npm install -g @letta-ai/letta-codeStart the server
Section titled “Start the server”Run App Server on a fixed local port during development:
letta app-server --listen ws://127.0.0.1:4500The process prints the base URL and the two channel URLs:
Listening on ws://127.0.0.1:4500Control: ws://127.0.0.1:4500/ws?channel=controlStream: ws://127.0.0.1:4500/ws?channel=streamYou can also omit --listen. App Server will bind to an available loopback port and print the selected URL.
Connect with the client helper
Section titled “Connect with the client helper”Use the exported App Server client for TypeScript integrations.
npm install @letta-ai/letta-code wsimport WebSocket from "ws";import { createAppServerClient } from "@letta-ai/letta-code/app-server-client";
const client = await createAppServerClient({ url: "ws://127.0.0.1:4500", WebSocket,}).connect();
client.onMessage((message, channel) => { console.log(channel, message.type);});The client opens both /ws?channel=control and /ws?channel=stream. Treat messages from both channels as the runtime event stream.
Start a runtime
Section titled “Start a runtime”Start a runtime for an existing agent and conversation:
const started = await client.runtimeStart({ 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", },});
if (!started.success || !started.runtime) { throw new Error(started.error ?? "Failed to start runtime");}
const runtime = started.runtime;Create a new conversation for an existing agent by omitting conversation_id and passing create_conversation:
const started = await client.runtimeStart({ agent_id: "agent-123", create_conversation: { body: {}, }, cwd: "/Users/me/project",});Create a new agent and conversation in one call:
const started = await client.runtimeStart({ create_agent: { body: { name: "App Server Agent", memory_blocks: [], }, }, create_conversation: { body: {}, }, cwd: "/Users/me/project", mode: "acceptEdits",});runtime_start_response.runtime is the canonical scope for future commands. Store and reuse it.
Send a turn
Section titled “Send a turn”Use runTurn when you want a promise that resolves when the turn completes:
const result = await client.runTurn({ runtime, payload: { kind: "create_message", messages: [ { role: "user", content: "Inspect this repository and summarize the test setup.", }, ], },});
console.log(result.stopReason);Use input when you want to fire the turn and manage completion from events yourself:
client.input({ runtime, payload: { kind: "create_message", messages: [ { role: "user", content: "Run the relevant tests.", }, ], },});Read stream events
Section titled “Read stream events”The main streamed output is stream_delta:
client.onMessage((message) => { if (message.type !== "stream_delta") return;
const delta = message.delta; if ("message_type" in delta && delta.message_type === "loop_error") { console.error(delta.message); return; }
console.log(delta);});stream_delta.delta can be either a standard Letta streaming message delta or a Letta Code lifecycle event such as tool starts/ends, command output, retries, status messages, and terminal stop reasons. Handle unknown delta shapes defensively and preserve them in logs while the protocol evolves.
Sync state
Section titled “Sync state”Call sync after reconnecting or when a UI needs a fresh snapshot:
await client.sync({ runtime, recover_approvals: false, force_device_status: true,});recover_approvals: false is useful for lightweight UI refreshes. Leave it unset when you want App Server to probe backend state for stale pending approvals.
Abort active work
Section titled “Abort active work”Cancel an active turn with abort_message:
const aborted = await client.abort({ runtime });console.log(aborted.aborted);Minimal raw websocket flow
Section titled “Minimal raw websocket flow”If you do not use the helper, send native JSON frames over the control socket.
{ "type": "runtime_start", "request_id": "runtime-1", "agent_id": "agent-123", "conversation_id": "conv-123", "cwd": "/Users/me/project", "mode": "standard"}{ "type": "input", "runtime": { "agent_id": "agent-123", "conversation_id": "conv-123" }, "payload": { "kind": "create_message", "messages": [ { "role": "user", "content": "Summarize this project." } ] }}Next steps
Section titled “Next steps”- Protocol lifecycle - Learn the command and event flow.
- External tools - Register tools that execute in your controller.
- Integration patterns - Design robust controllers and team orchestrators.