npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@letta-ai/letta-agent-sdk

v0.2.6

Published

SDK for programmatic control of Letta agents

Readme

Letta Agent SDK

npm Discord

The SDK interface to Letta Code. Build agents with persistent memory that learn over time.

Installation

npm install @letta-ai/letta-agent-sdk

Quick 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