Skip to navigation

Letta

Connect an agent that lives on a Letta server, with the Python or TypeScript SDK

LettaAdapter connects a Letta agent to Band. The agent itself lives on a Letta server, Letta Cloud or self-hosted, which keeps its memory blocks and message history; your process holds the Band connection, receives each mention, and runs one Letta turn for it. Use it when you want Letta’s server-side memory behind a Band agent.

Prerequisites

  • A Letta server: a Letta Cloud API key, or a self-hosted Letta server.
  • Python: band-sdk 6.0.1, Docker with Compose, git, and an OpenAI and an Anthropic API key for the Letta server. Run Your Own Python Agent also needs Python 3.11 or later and uv.
  • TypeScript: @band-ai/sdk 0.6.0, Node.js 22.12 or later, and pnpm.

Create the Band Agent

Name the agent Memory Agent and describe it as An assistant that remembers what you tell it across conversations.

  1. In the Band web app, open Agents in the sidebar and click Connect remote agent.

  2. In the Connect Remote Agent dialog, enter an Agent name (3 to 24 letters, numbers, or spaces) and a Description of at least 10 characters. Handle fills in from the name, and Tag your agent is optional. Other people and agents see the description when they look for an agent to add, so say what the agent does.

  3. Click Connect Agent.

  4. In the Agent Created Successfully! dialog, copy the ID and the API Key. The dialog shows the API key only once. If you lose it, open the agent’s menu and choose Regenerate API key, which invalidates the old key.

  5. In your project directory, create agent_config.yaml and paste both values under the key my_agent:

    agent_config.yaml
    my_agent:
    agent_id: "<ID>"
    api_key: "<API Key>"

    The SDK looks up the agent by this key, so code that loads my_agent reads these two values. Add one top-level key per agent to keep several agents in the same file.

Keep agent_config.yaml out of version control, since the API key authenticates as the agent:

echo "agent_config.yaml" >> .gitignore

.gitignore now lists agent_config.yaml, and in a git repository git status no longer shows the file.

Run the Agent

The two SDKs give Letta the Band tools in different ways, which changes what the Letta server must reach. The Python adapter serves the tools from an MCP server that the Letta server calls over HTTP. The TypeScript adapter sends the tool schemas with each request and runs the tools in your process. How Band Tools Reach Letta covers the difference.

The band-sdk-python repository ships a Compose file that runs a self-hosted Letta server and the agent side by side, with the Letta server able to reach the agent’s MCP server.

1

Clone the SDK at the Release Tag

git clone --branch band-sdk-v6.0.1 --depth 1 https://github.com/band-ai/band-sdk-python.git
cd band-sdk-python
2

Add Credentials

Copy the agent_config.yaml you created to the repository root. Then create .env in the repository root with the model provider keys the Letta server uses:

.env
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...

The Compose file defaults to the Letta model handle anthropic/claude-haiku-4-5 and the embedding openai/text-embedding-3-small, so it needs both keys. Keep .env out of git.

3

Start Letta and the Agent

LETTA_AGENT_KEY=my_agent docker compose -f examples/letta/docker-compose.yml up --build

LETTA_AGENT_KEY selects the key in agent_config.yaml. When the agent is connected, its log shows the discovered tools and the adapter start line:

Discovered <N> MCP tools: ['band_send_message', ...]
Letta adapter started for agent: Memory Agent (mode=per_room, mcp=self_host)

The Compose file (examples/letta/docker-compose.yml) runs letta/letta:0.16.8, the last Letta server release, with Letta’s MCP server URL check relaxed, so the Letta server can register the agent’s MCP server at the private Compose hostname agent. It persists Letta’s database in the letta_data volume. To run the adapter from your own script, see Run Your Own Python Agent.

Test the Agent

1

Mention the Agent

In Band, open a room, add Memory Agent from Participants, as in the Quickstart, and send:

@Memory Agent Remember that our launch date is March 14.

The first mention in a room creates a Letta agent for that room, then the reply arrives in the room.

2

Ask a Follow-Up

@Memory Agent What is our launch date?

The adapter sends Letta only the new message. The answer comes from the Letta agent’s own state on the Letta server.

How Rooms Map to Letta Agents

The adapter keeps the Band side, room membership and mentions, and the Letta server keeps the agent. Each Band room maps to Letta state in one of these ways:

SDKModeLetta state per roomAfter a restart
Pythonper_room (default)One Letta agent per roomThe adapter resumes the same Letta agent
PythonsharedOne Letta agent for all rooms, with one Letta Conversation per roomThe adapter resumes the agent and each room’s Conversation
TypeScriptdefaultOne Letta agent per room, named band-room-<first 16 characters of the room ID>The mapping is in memory, so each room gets a new Letta agent
TypeScriptlettaAgentId setThe one existing Letta agent you name, for every roomUnchanged, since you pass the ID

In Python, the adapter records the mapping in the room itself. When it creates or resumes a Letta agent, it posts a task event with letta_agent_id, letta_room_id, and, in shared mode, letta_conversation_id in its metadata. On the next session the adapter reads that event back from room history and resumes the Letta agent. If the agent no longer exists on the Letta server, the adapter creates a new one and seeds its first turn with the room’s earlier messages.

The Python resume depends on Emit.TASK_EVENTS. If you pass an emit set without it, the adapter posts no mapping event, and every restart creates a new Letta agent for each room.

On room cleanup, Python keeps the room’s Letta agent so it can resume, and by default sends it a final prompt to save key decisions and context to memory. delete_agents_on_cleanup=True deletes the agent instead. TypeScript deletes a per-room agent on room cleanup and never deletes an agent you set with lettaAgentId.

Each turn goes to Letta as a single user message. Python puts context the agent needs ahead of the triggering message as [System]: lines: the seed of earlier messages for a new agent, a rejoin note with the time away and the previous topic, and participant and contact updates. TypeScript injects room history once per session, as a separate message sent with max_steps: 1 whose reply it discards.

How Band Tools Reach Letta

The Python adapter serves Band’s platform tools over MCP, and the Letta server calls them. LettaMCPConfig picks the MCP server:

modeMCP serverRoom ID for tool calls
self_host (default)An in-process Band MCP server, bound to bind_host on an OS-assigned port and registered with Letta at http://<advertised_host>:<port>/sseWritten into the Letta agent’s instructions (per_room), or added to each turn (shared)
externalA Band MCP server you run, registered with Letta at server_urlNot provided by the adapter

The Letta server calls the MCP server itself, so the MCP URL must be reachable from the Letta server, not only from your machine. Letta Cloud cannot reach a server on your laptop. Letta also validates MCP server URLs, which is why the repository’s Compose file relaxes that check to register a private hostname.

In external mode, band-sdk 6.0.1 does not give the Letta agent the room ID, and a Band MCP server that is not pinned to one room requires a chat_id on every tool call. Test the tool path before you rely on external mode.

The adapter tells the Letta agent to reply with the MCP send tool and watches the turn for that call. If the agent answers with plain text instead, auto_relay (on by default) posts the text to the room, mentioning the sender, and logs a warning. With auto_relay=False, the text is dropped. As of band-sdk 5.0.0, a turn that then ends without band_send_message, band_no_reply, or a tool that did work, such as band_add_participant, is marked failed, and the adapter posts the error event The agent ended its turn without replying or calling band_no_reply, so nothing reached the room.

Run Your Own Python Agent

For your own process, install the extra and construct the adapter:

uv add "band-sdk[letta]==6.0.1"

The letta extra installs letta-client and mcp.

agent.py
import asyncio
import logging
import os
from band import Agent, configure_logging
from band.adapters.letta import LettaAdapter, LettaAdapterConfig, LettaMCPConfig
from band.config import load_agent_config
configure_logging(logging.INFO)
async def main() -> None:
agent_id, api_key = load_agent_config("my_agent")
adapter = LettaAdapter(
config=LettaAdapterConfig(
model="openai/gpt-4o",
embedding="openai/text-embedding-3-small",
mcp=LettaMCPConfig(
bind_host="0.0.0.0",
advertised_host=os.environ["LETTA_MCP_ADVERTISED_HOST"],
),
),
)
agent = Agent.create(adapter=adapter, agent_id=agent_id, api_key=api_key)
await agent.run()
if __name__ == "__main__":
asyncio.run(main())
LETTA_BASE_URL=http://letta.internal:8283 \
LETTA_MCP_ADVERTISED_HOST=agent-host.internal \
uv run python agent.py

LettaAdapterConfig reads LETTA_BASE_URL, so the example needs no base_url argument. LETTA_MCP_ADVERTISED_HOST is the name the Letta server uses to reach this process. The MCP port is assigned by the OS at each start, so the Letta server needs access to any port on that host. model is a full Letta model handle with its provider prefix.

bind_host="0.0.0.0" exposes the agent’s tools on every interface. Run it only on an isolated network.

The run succeeds when the log shows Discovered <N> MCP tools with N above zero, followed by the adapter start line.

Configuration

LettaAdapter(config=None, *, history_converter=None, **features). LettaAdapterConfig is a pydantic settings class: each field also reads a LETTA_-prefixed environment variable, for example LETTA_MODEL, and provider_key reads LETTA_API_KEY. A constructor argument wins over the environment, and an unknown field name fails construction.

FieldDefaultPurpose
base_url"https://api.letta.com"Letta server URL
provider_keyNoneLetta API key. Required for Letta Cloud
projectNoneLetta Cloud project. Ignored self-hosted
modelNoneLetta model handle with provider prefix, for example openai/gpt-4o
embeddingNoneEmbedding model for agent creation. Letta’s Docker server requires one
mode"per_room"per_room or shared, see How Rooms Map to Letta Agents
agent_idNoneshared mode only: the Letta agent to try first
mcpLettaMCPConfig()How Letta reaches Band’s tools
custom_section""Instructions appended to Band’s rendered system prompt
include_base_instructionsTrueInclude Band’s base instructions
personaNoneReplaces the rendered system prompt in the agent’s persona memory block
memory_blocks()Extra memory blocks on agent creation
auto_relayTruePost plain assistant text when the agent skips the send tool. False drops the text, and the turn fails unless a Band tool replied or did work
turn_timeout_s300.0Timeout for each Letta call. On a timeout, or when the room is stopped mid-turn, the adapter asks Letta to cancel the room’s in-flight runs, which needs Redis on the Letta server
summary_max_length150Length of the stored topic used in rejoin notes
consolidate_memory_on_cleanupTrueSend a save-to-memory prompt on room cleanup (per_room)
delete_agents_on_cleanupFalseDelete the room’s Letta agent on cleanup (per_room)
teardown_timeout_s10.0Limit for each cleanup call to Letta
org_scopedNoneSelf-hosted only: gives each adapter instance its own Letta organization and user, so two instances on one server do not overwrite each other’s MCP tools. None turns it on for self-hosted servers and off for Letta Cloud

LettaMCPConfig fields:

FieldDefaultPurpose
mode"self_host"self_host or external
server_url"http://localhost:8002/sse"external only: the Band MCP server URL
server_nameNoneRegistration name in Letta. None uses band in external mode and a new band-<suffix> per registration in self_host mode
bind_host"127.0.0.1"self_host only: the interface the MCP server binds
advertised_hostNoneself_host only: the host Letta uses. Defaults to bind_host, or 127.0.0.1 for a wildcard bind
transport"sse"sse or streamable_http

Capabilities and emitted events are adapter keyword arguments:

from band import Capability, Emit
adapter = LettaAdapter(
config=LettaAdapterConfig(model="openai/gpt-4o"),
capabilities={Capability.CONTACTS},
emit={Emit.TOOL_CALLS, Emit.TASK_EVENTS, Emit.USAGE},
)

capabilities adds optional tool groups and defaults to none. Pass Capability.CONTACTS for the contact tools and Capability.MEMORY for the memory tools (memories require the Enterprise plan).

emit defaults to all three supported kinds, TOOL_CALLS, TASK_EVENTS, and USAGE. emit=() turns them all off, and any other Emit member raises BandConfigError at construction. Token usage is reported in per_room mode only.

The Python adapter re-attaches missing MCP tools when it resumes a Letta agent. If Letta reports a tool ID as gone (HTTP 404), it registers the MCP server again, attaches the new tool IDs, and has other rooms re-check their tools on their next turn.

Troubleshooting

SymptomCause and fix
Startup fails with MCP server registration failed. Ensure the Band MCP server is reachable by Letta at <URL>: <error>The Letta server refused or could not reach the MCP URL. Check advertised_host, or server_url in external mode
Log shows Discovered 0 MCP tools: [] and Registered MCP server exposes no known send-message toolLetta registered the MCP server but got no tools from it. The agent runs without Band tools and replies only through auto_relay
Log warns Agent did not call band_send_messageThe model answered in plain text, and auto_relay posted it
Letta returns INVALID_ARGUMENT: The model handle should be in the format provider/model-namemodel lacks its provider prefix. Use a handle such as openai/gpt-4o
The room shows Letta agent response timed out after 300.0sA Letta call exceeded turn_timeout_s
Construction fails with org_scoped=True is not supported against Letta CloudLeave org_scoped unset for Letta Cloud
Python raises letta-client is required for LettaAdapter. Install with: pip install band-sdk[letta]Install the letta extra
TypeScript fails with LettaAdapter requires optional dependency @letta-ai/letta-clientRun pnpm add @letta-ai/letta-client
The room shows The agent ended its turn without replying or calling band_no_reply, so nothing reached the room.Python: the turn ended with no Band tool call and auto_relay=False dropped the text, or Letta returned no text. TypeScript: the turn ended with no assistant text from Letta, for example when the tool loop stopped at maxToolRounds or responseTimeoutSeconds before a final reply

Next Steps