@letta-ai/letta-agent-sdk
v0.2.6
Published
SDK for programmatic control of Letta agents
Keywords
Readme
Letta Agent SDK
The SDK interface to Letta Code. Build agents with persistent memory that learn over time.
Installation
npm install @letta-ai/letta-agent-sdkQuick start
Client creation
import { LettaAgentClient } from "@letta-ai/letta-agent-sdk";
// Local: SDK-owned Letta Code app-server over loopback websockets. The SDK
// spawns/manages the app-server process for you.
const localClient = new LettaAgentClient({ backend: "local" });
// Remote: connect to a user-managed Letta Code app-server over websockets.
const remoteClient = new LettaAgentClient({
backend: "remote",
url: "http://127.0.0.1:4500",
// Required when the app-server is bound to a non-loopback interface with
// --ws-auth capability-token.
authToken: process.env.LETTA_APP_SERVER_TOKEN,
});
// Constellation: create or resume agents whose state lives in Letta's
// agent cloud, with an SDK-managed sandbox by default.
const client = new LettaAgentClient({
backend: "cloud",
apiKey: process.env.LETTA_API_KEY,
});Persistent agent with multi-turn conversations
import { LettaAgentClient } from "@letta-ai/letta-agent-sdk";
const client = new LettaAgentClient({ backend: "local" });
const agentId = await client.createAgent({
persona: "You are a helpful coding assistant for TypeScript projects.",
});
await using session = client.resumeSession(agentId);
await session.send("Find and fix the bug in auth.ts");
for await (const msg of session.stream()) {
if (msg.type === "assistant") console.log(msg.content);
}
await session.send("Add a unit test for the fix");
for await (const msg of session.stream()) {
if (msg.type === "assistant") console.log(msg.content);
}Cloud, remote, and local agent sessions can accept another send() while a
turn is streaming. The SDK sends the same input frame used by Letta Desktop
and the listener owns queueing; stream() may surface queue_update events
before the current turn's result.
By default, resumeSession(agentId) continues the agent’s default conversation.
Use createSession(agentId) when you want to start a fresh thread.
For Constellation agents, omitting environment lets the SDK create and manage
a sandbox for the session. environment is still session-scoped and can
override the client default when you want to use a specific remote runtime:
await using session = client.resumeSession(agentId, {
environment: { name: "LettaDevelopers" },
});The top-level helpers (createAgent, createSession, resumeSession, and
prompt) remain available. Local helper calls use Letta Code's default agent
selection when you do not pass an agent ID.
User-managed app-server backend
Use backend: "remote" when you already have a Letta Code app-server running.
The app-server URL selects the execution environment; the SDK uses the same
Letta Code websocket protocol for runtime_start, input, streaming deltas,
and SDK-defined external tools.
const client = new LettaAgentClient({
backend: "remote",
url: "http://127.0.0.1:4500",
authToken: process.env.LETTA_APP_SERVER_TOKEN,
requestTimeoutMs: 120_000,
});
const agentId = await client.createAgent({
model: "anthropic/claude-sonnet-4",
persona: "You are a helpful coding assistant.",
});
await using session = client.createSession(agentId);
await session.send("Summarize this repository.");
for await (const msg of session.stream()) {
if (msg.type === "assistant") console.log(msg.content);
}Constellation
Use backend: "cloud" to create or resume agents hosted on Constellation. If
no environment is provided, the SDK creates a managed sandbox, waits for it to
come online, and refreshes it while the session is active. Non-default
conversations request a conversation-scoped sandbox from supporting servers;
the default conversation and legacy servers retain the agent-scoped lifecycle.
Managed sandboxes are left for TTL cleanup by default so another session for the
same conversation can reconnect to them.
const client = new LettaAgentClient({
backend: "cloud",
apiKey: process.env.LETTA_API_KEY,
sandbox: {
// Optional: defaults to a 5-minute refresh TTL.
ttlMinutes: 5,
// Optional: defaults false. Set true only with exclusive ownership.
terminateOnClose: false,
},
});
const agentId = await client.createAgent({
model: "anthropic/claude-sonnet-4",
persona: "You are a helpful coding assistant.",
});
await using session = client.resumeSession(agentId, {
permissionMode: "unrestricted",
});
await session.send("Summarize this repository.");
for await (const msg of session.stream()) {
if (msg.type === "assistant") console.log(msg.content);
}If you pass cwd for a Constellation session, use a path that exists inside the
selected remote environment or managed sandbox. Local paths such as
process.cwd() are not mapped into managed sandboxes automatically.
You can still set a default environment on the client or override it per
session to use an existing remote runtime instead of an SDK-managed sandbox. Use
the environment name from letta remote --env-name <name>:
const client = new LettaAgentClient({
backend: "cloud",
apiKey: process.env.LETTA_API_KEY,
environment: { name: "devbox" },
});
await using session = client.resumeSession(agentId, {
environment: { name: "devbox" },
});For advanced cases where you want to target a specific remote connection, pass
its connectionId instead. Connection IDs are assigned when the remote listener
registers and may change after reconnects:
await using session = client.resumeSession(agentId, {
environment: { connectionId: "conn-123" },
});environment also accepts { id: "env-..." } for an environment record or
{ deviceId: "device-..." } for a stable device selector.
environment and sandbox are mutually exclusive. Conversation-scoped
sandboxes refresh and terminate by sandbox ID. Legacy agent-scoped sandboxes
retain the latest-active ownership check. Pass sandbox.terminateOnClose: true
to request best-effort eager cleanup, but only when no other session or
reconnecting client needs the sandbox.
If Cloud reaps a conversation-scoped sandbox before the next refresh,
send() throws CloudManagedSandboxExpiredError before transmitting the turn.
Close the old SDK session and resume the same conversation to create a fresh
connection, then retry safely:
import {
CloudManagedSandboxExpiredError,
LettaAgentClient,
} from "@letta-ai/letta-agent-sdk";
const client = new LettaAgentClient({
backend: "cloud",
apiKey: process.env.LETTA_API_KEY,
});
const conversationId = "conv-123";
let session = client.resumeSession(conversationId);
async function sendWithSandboxRecovery(message: string): Promise<void> {
for (let attempt = 0; attempt < 2; attempt += 1) {
try {
await session.send(message);
for await (const event of session.stream()) {
if (event.type === "assistant") console.log(event.content);
}
return;
} catch (error) {
if (!(error instanceof CloudManagedSandboxExpiredError) || attempt > 0) {
throw error;
}
session.close();
session = client.resumeSession(conversationId);
}
}
}Only retry automatically for this pre-send expiration error. If a connection
fails after send() succeeds, inspect conversation history before retrying
because the original message may already have reached the runtime.
By default, websocket authentication uses Authorization headers. Set
webSocketAuth: "query" for browser-style websocket clients that cannot send
custom upgrade headers.
Cloud repositories
Cloud clients can create hosted repositories, manage text files inside them, and attach repositories to a session as resources. Repository resources are attached before the session starts and detached when the SDK session closes.
const client = new LettaAgentClient({
backend: "cloud",
apiKey: process.env.LETTA_API_KEY,
});
const repo = await client.repositories.create({ name: "inputs" });
await client.repositories.files.create(repo.id, {
path: "data.csv",
content: csvContent,
});
await using session = client.createSession(agentId, {
resources: [
{ type: "repository", repositoryId: repo.id },
],
});
await session.send("Analyze the files in the attached repository.");Repository file helpers are available under client.repositories.files, and
version history helpers are available under client.repositories.versions.
Session configuration
Session options let you set runtime defaults before a session starts, including
model, reasoningEffort, cwd, permissionMode, and dreaming triggers. For
remote and Constellation sessions, cwd must be a path inside the selected
runtime environment.
import { LettaAgentClient } from "@letta-ai/letta-agent-sdk";
const client = new LettaAgentClient({ backend: "local" });
const session = client.resumeSession("agent-123", {
model: "anthropic/claude-sonnet-4",
reasoningEffort: "high",
// For local sessions this may be a local path; for remote/Constellation
// sessions, use a path inside the selected runtime environment.
cwd: "/workspace/project",
permissionMode: "unrestricted",
dreaming: {
trigger: "step-count", // off | step-count | compaction-event
stepCount: 8,
},
});You can also inspect and change models after startup:
const catalog = await session.listModels();
await session.updateModel({ model: "sonnet", reasoningEffort: "medium" });Call await session.abort() to interrupt the current turn without closing the
session.
For advanced protocol access, use sendCommand() with raw Letta Code websocket
protocol commands:
await session.sendCommand({
type: "change_device_state",
runtime: { agent_id: session.agentId!, conversation_id: session.conversationId! },
payload: { cwd: "/workspace/project" },
});
const sync = await session.sendCommand(
{
type: "sync",
runtime: { agent_id: session.agentId!, conversation_id: session.conversationId! },
},
{ responseType: "sync_response" },
);Dreaming
The dreaming API has three explicit components: collect normalized transcript snapshots, initialize a guarded memory filesystem, then run a dream over the exact transcript list. Reflection batches edit isolated clones and a final aggregation pass synthesizes their diffs into the dream agent's MemFS.
import {
LettaAgentClient,
collectTranscripts,
dream,
initDreamAgent,
} from "@letta-ai/letta-agent-sdk";
const client = new LettaAgentClient({
backend: "local",
appServer: { harnessBackend: "api" },
});
// 1. Collect immutable snapshots after an exclusive cutoff. Limits apply per
// source, so this selects the five newest eligible sessions from each store.
const transcripts = await collectTranscripts({
after: "2026-07-01T00:00:00Z",
sources: [
{ type: "claude", limit: 5 },
{ type: "codex", limit: 5 },
],
});
// 2. Initialize the complete starting MemFS. Seeded files may be modified but
// not deleted. New files and directories are allowed only below skills/.
const agent = await initDreamAgent(client, {
model: "anthropic/claude-opus-4-8",
memfs: {
directories: ["skills"],
files: {
"system/project/AGENTS.md":
"---\ndescription: Project guidance\n---\n\n# Project guidance\n",
},
},
guard: {
allowedNewFilePrefixes: ["skills"],
},
});
// 3. Run against the exact snapshots. The optional prompt is appended to each
// reflection batch, not to the aggregation pass.
const result = await dream({
client,
agentId: agent.agentId,
transcripts,
reflectionPrompt: "Only retain learnings relevant to this project.",
});
if (result.kind === "completed") {
console.log(result.success, result.runRoot, result.aggregation.report);
}Available transcript sources include claude, codex, openhands, letta,
and already-normalized transcript files. after compares against each
session's end time and is exclusive; limit keeps the latest eligible sessions
for that source spec. Use planOnly: true to inspect transcript selection and
batch packing without running agents.
The pre-commit guard is installed on the canonical MemFS and every reflection
clone. The SDK also validates every commit and the final working tree after
each run, so --no-verify cannot bypass the policy. For finer control, pass
guard.allowedOperations rules for exact or recursive paths. Every run records
batch inputs, output clones, diffs, normalized worker trajectories, reports,
and an immutable final aggregate snapshot under runRoot. See
examples/dream.ts.
OpenHands conversations retain incremental cursors in the dream agent's SDK state; later dreams skip records at or before the last successfully reflected timestamp. Claude and Codex sessions are finite snapshots and remain cursor-free.
Links
- Docs: https://docs.letta.com/letta-agent-sdk
- Examples:
./examples
Made with 💜 in San Francisco
