Skip to content
Letta Code Letta Code Letta Docs
Sign up
App Server

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.

Terminal window
npm install -g @letta-ai/letta-code

Run App Server on a fixed local port during development:

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

The process prints the base URL and the two channel URLs:

Listening on ws://127.0.0.1:4500
Control: ws://127.0.0.1:4500/ws?channel=control
Stream: ws://127.0.0.1:4500/ws?channel=stream

You can also omit --listen. App Server will bind to an available loopback port and print the selected URL.

Use the exported App Server client for TypeScript integrations.

Terminal window
npm install @letta-ai/letta-code ws
app-server-client.ts
import 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 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.

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.",
},
],
},
});

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.

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.

Cancel an active turn with abort_message:

const aborted = await client.abort({ runtime });
console.log(aborted.aborted);

If you do not use the helper, send native JSON frames over the control socket.

runtime_start
{
"type": "runtime_start",
"request_id": "runtime-1",
"agent_id": "agent-123",
"conversation_id": "conv-123",
"cwd": "/Users/me/project",
"mode": "standard"
}
input
{
"type": "input",
"runtime": {
"agent_id": "agent-123",
"conversation_id": "conv-123"
},
"payload": {
"kind": "create_message",
"messages": [
{
"role": "user",
"content": "Summarize this project."
}
]
}
}