Skip to content
Discord
Letta Agent SDK

Sessions, turns, and durability

The agent, conversation, and session model, and what the SDK guarantees across connections

An agent is the persistent entity with memory. A conversation is a thread on that agent. A session is the active connection you use to send messages and stream events.

  • Agent IDs look like agent-xxx on the cloud backend and agent-local-xxx on the local backend. Conversation IDs look like conv-xxx.
  • createAgent() also creates the agent’s default conversation, so a new agent is immediately usable.
  • createSession(agentId) starts a new conversation on an existing agent.
  • resumeSession(id) accepts either kind of ID: pass an agent-xxx ID to resume the agent’s default conversation, or a conv-xxx ID to resume a specific conversation.
// New conversation on an existing agent
await using session = client.createSession(agentId);
// Later: save the thread and pick it up again
const conversationId = session.conversationId;
// → "conv-cff81390-0763-431d-b24e-b218ae37ec81"
await using resumed = client.resumeSession(conversationId);
// Or resume the agent's default conversation
await using main = client.resumeSession(agentId);

Sessions expose read-only agentId, conversationId, and sessionId values once the backend resolves them (sessionId is "<agentId>:<conversationId>"). You can also create and inspect conversations without opening a session through client.conversations (create, list, retrieve, update, listMessages).

A turn is one send() plus one pass through stream():

  1. send() transmits the user message (or queues it if a turn is already streaming — the runtime owns the queue).
  2. stream() yields the turn’s typed events — reasoning, tool calls, tool results, assistant text — and terminates after the turn’s result message.
  3. The result carries the turn’s full final text, success, stopReason, durationMs, and the runIds the turn executed.

See Sending messages for the full event contract with captured payloads.

  • abort() stops the active turn without closing the session; the stream still delivers a result for the aborted turn.
  • close() (or await using disposal) releases the session and its local resources: MCP connections, client tools, and any session-scoped repository resources links.
  • A session whose connection closed unexpectedly cannot be reused. The stream emits an error followed by a failed result; call resumeSession(conversationId) to continue in a new session.
Persists on the agent/conversationScoped to the session (gone on close)
Memory blocks, MemFS, conversation historyClient tools and MCP servers
model / reasoningEffort updates passed to sessionscanUseTool callbacks and permission behavior
Persistent repository attachments (agents.repositories)Session resources repository links
Pending tool approvals (recoverable in a new session)cwd, env, computer/sandbox selection
  • Messages you send() while a turn is active are queued by the runtime, not dropped; stream() may emit queue_update events, and removeQueuedMessage(itemId) waits for the authoritative queue response.
  • Pending approvals survive disconnects — recover them in a new session with recoverPendingApprovals() (see Permissions).
  • The SDK does not replay events missed while disconnected. After resuming, reconcile with listMessages() or fetch a consolidated snapshot with bootstrapState().
  • If a connection fails after send() succeeded, do not blindly retry — the message may already have reached the runtime. Inspect the conversation history first. The one automatic retry the docs recommend is the pre-send CloudManagedSandboxExpiredError case in Deployment.
  • Every message carries runIds (and result.runIds lists the turn’s runs), so you can correlate events, history, and retries.