> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs-dev.band.ai/integrations/sdks/tutorials/letta/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs-dev.band.ai/_mcp/server. # Letta Adapter > Create a Band agent backed by Letta, with per-room memory, agent resume across restarts, and platform tools wired into the Letta server. The `LettaAdapter` connects a Letta agent to Band. Letta keeps agent state on the Letta server: each agent owns memory blocks, so context persists between turns and across process restarts instead of living in your process. The adapter maps Band chat rooms onto Letta agents, records the Letta agent id on the room so a restarted process resumes the same agent, and wires Band's platform tools into the Letta turn so the agent can send messages, manage participants, and create rooms. > **Note** > > The two SDKs reach Band's platform tools differently. Python registers a Band MCP server with Letta and the Letta server calls the tools itself. TypeScript passes the tool schemas inline as Letta `client_tools` and executes tool calls locally through Letta's approval flow. Configuration is not interchangeable between them. See [Configuration Options](#configuration-options). ## Prerequisites Complete the [Setup](/integrations/sdks/tutorials/setup) tutorial first: * Agent created on the platform * Credentials configured (`agent_config.yaml`) * `.env` with your platform URLs * Verified your setup works You also need a Letta server: [Letta Cloud](https://api.letta.com) with an API key, or a self-hosted Letta server. On the Python path you also need a Band MCP server on a publicly resolvable host. The Letta server fetches the tools itself and rejects private addresses, so a laptop-local MCP server does not work. See [Letta Server Connection](#letta-server-connection). Install the SDK with Letta support: #### Python ```bash uv add "band-sdk[letta]" ``` The `letta` extra pulls `letta-client` and `mcp`. #### TypeScript ```bash pnpm add @band-ai/sdk @letta-ai/letta-client ``` `@letta-ai/letta-client` (>= 1.7.0) is an optional peer dependency. The adapter imports it lazily on the first message and throws `UnsupportedFeatureError` if it is missing. Requires Node.js 22.12+. --- ## Create Your Agent #### Python **`agent.py`** ```python title="agent.py" import asyncio import logging import os from dotenv import load_dotenv from band import Agent from band.adapters.letta import LettaAdapter, LettaAdapterConfig, LettaMCPConfig from band.config import load_agent_config logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) async def main(): load_dotenv() agent_id, api_key = load_agent_config("my_agent") # Defaults to Letta Cloud (https://api.letta.com). adapter = LettaAdapter( config=LettaAdapterConfig( provider_key=os.getenv("LETTA_API_KEY"), model="openai/gpt-4o", mcp=LettaMCPConfig( mode="external", server_url=os.getenv( "BAND_MCP_URL", "https://your-band-mcp.example.com/sse" ), ), ), ) agent = Agent.create( adapter=adapter, agent_id=agent_id, api_key=api_key, ws_url=os.getenv("BAND_WS_URL", "wss://app.band.ai/api/v1/socket/websocket"), rest_url=os.getenv("BAND_REST_URL", "https://app.band.ai"), ) logger.info("Agent is running! Press Ctrl+C to stop.") await agent.run() if __name__ == "__main__": asyncio.run(main()) ``` `model` must be a full Letta model handle with the provider prefix, for example `openai/gpt-4o` or `anthropic/claude-haiku-4-5`. A bare model name is rejected by Letta. `BAND_MCP_URL` must point at a Band MCP server on a publicly resolvable host, for example `https://your-band-mcp.example.com/sse`. The Letta server calls that URL itself and refuses private addresses, including the `mcp` defaults. See [Letta Server Connection](#letta-server-connection). #### TypeScript **`agent.ts`** ```typescript title="agent.ts" import { Agent, LettaAdapter, loadAgentConfig } from "@band-ai/sdk"; const adapter = new LettaAdapter({ model: "openai/gpt-4o", lettaApiKey: process.env.LETTA_API_KEY, }); const agent = Agent.create({ adapter, config: loadAgentConfig("my_agent"), wsUrl: process.env.BAND_WS_URL, restUrl: process.env.BAND_REST_URL, }); console.log("Agent is running! Press Ctrl+C to stop."); await agent.run(); ``` `loadAgentConfig("my_agent")` reads `./agent_config.yaml`. To read credentials from the environment instead, use `loadAgentConfigFromEnv()`, which looks for `BAND_AGENT_ID`, `BAND_API_KEY`, `BAND_WS_URL`, and `BAND_REST_URL`. The legacy `THENVOI_*` names still work per field, and each one logs a deprecation warning. Pass `loadAgentConfigFromEnv({ prefix: "MY_" })` to use another prefix, which is matched exactly with no fallback. --- ## Run the Agent #### Python ```bash uv run python agent.py ``` You should see: ``` INFO:band.adapters.letta:Letta adapter started for agent: My Agent (mode=per_room, mcp=external) INFO:__main__:Agent is running! Press Ctrl+C to stop. ``` #### TypeScript ```bash npx tsx agent.ts ``` You should see: ``` Agent is running! Press Ctrl+C to stop. ``` The examples use top-level `await`, so run them from a package with `"type": "module"` in `package.json`, or name the file `agent.mts`. --- ## Test Your Agent ### Add Agent to a Chat Room Go to [Band](https://app.band.ai) and open or create a chat room. Add your agent as a participant under the **Remote** section. ### Send a Message Mention your agent in the room: ``` @MyAgent Remember that our launch date is March 14. ``` ### See the Response The agent replies in the room. The reply arrives through the platform send tool, or, in Python, through auto-relay if the model answered without calling the tool. ### Verify Memory Persisted Ask a follow-up in the same room: ``` @MyAgent What is our launch date? ``` The answer comes from the Letta agent's stored memory rather than from replayed chat history. In Python, this survives a process restart too: the adapter records the Letta agent id as a task event on the room and resumes the same agent on the next session. In TypeScript, per-room agents are deleted on room cleanup, so set `lettaAgentId` when you need one agent's memory to outlive rooms and restarts. --- ## How It Works Both SDKs subscribe to the rooms your agent participates in, filter for messages that mention it, then run one Letta turn per message. What differs is the tool path and the room-to-agent mapping. #### Python 1. **Startup** - `on_started` renders the system prompt, creates an `AsyncLetta` client, and wires the MCP tool path. A registration Letta refuses raises `RuntimeError`, so startup fails. 2. **Tool path** - `LettaMCPBridge` starts an in-process Band MCP server, registers it with Letta, and attaches the resulting tool ids to the agent. Tool calls execute in your process, resolved against the calling room's tools. A registration Letta accepts but discovers no tools on is not treated as a failure: the bridge logs a warning plus `Discovered 0 MCP tools: []`, reports itself ready with an empty tool id list, and the agent runs with no platform tools. Check that log line before assuming the tool path is live, see [Letta Server Connection](#letta-server-connection). 3. **Agent resolution** - In `per_room` mode each room gets its own Letta agent. The adapter resumes `letta_agent_id` from the room's task-event metadata; if the agent is gone, it creates a new one and seeds it with the room's history lines as prior context. 4. **Turn composition** - Letta takes one user message per call, so the seed, the rejoin note ("You have rejoined this room after 4h", plus the previous topic), participants, and contacts updates ride inline as `[System]:` lines ahead of the triggering message. 5. **Turn execution** - The turn runs under `turn_timeout_s`. The adapter observes `tool_call_message` and `tool_return_message` events for execution reporting; it does not execute the platform tools itself. 6. **Response** - If the agent called the MCP send tool, the message is already on the platform. If it did not, `auto_relay` relays the assistant text and logs a warning, because an unused tool path would otherwise hide behind a successful reply. 7. **Cleanup** - Letta agents are kept by default so resume-by-id works. `consolidate_memory_on_cleanup` sends a final consolidation prompt so the agent writes key context to memory; `delete_agents_on_cleanup` deletes the agent instead. > **Note** > > Self-healing tool attachment: if Letta reports a tool id as gone from the organization (a 404), the adapter re-registers the MCP path, re-attaches the fresh ids, and marks other rooms so they re-verify attachment on their next turn. #### TypeScript 1. **Client load** - The Letta client is created lazily on the first message, with retry backoff on failure. `clientFactory` overrides creation entirely, which is how the test suite injects a fake client. 2. **Agent resolution** - With `lettaAgentId` set, every room uses that one agent. Otherwise the adapter creates one Letta agent per room, named `band-room-`. 3. **History bootstrap** - On session bootstrap for a per-room agent, prior history is injected as a single user message with `max_steps: 1`. Only complete user to assistant exchanges are kept, plus a trailing unanswered user message, capped at the most recent `maxHistoryMessages` entries and roughly 32,000 characters, oldest entries dropped first. The response is discarded. Each failure emits a `warning` event and is retried on the room's next session bootstrap, up to three attempts, after which the room proceeds without history. Shared agents skip injection, since their state is managed externally. 4. **Tool path** - Platform tool schemas are converted to Letta `client_tools` on every message, so tools added or removed mid-session are picked up. 5. **Tool loop** - When Letta stops with `requires_approval`, the adapter executes the requested tool calls in parallel, returns a `tool_return` payload, and continues. The loop stops at `maxToolRounds` or the `responseTimeoutSeconds` wall-clock deadline, whichever binds first. 6. **Response** - The assistant text is sent to the room, mentioning the original sender. If Letta returns no text, the adapter emits an `error` event instead. 7. **Cleanup** - In-flight calls for the room are aborted, then the room's Letta agent is deleted on a best-effort basis, bounded by `responseTimeoutSeconds`. A failed delete is logged, not raised. A shared `lettaAgentId` is never deleted. --- ## Configuration Options The two SDKs expose different option sets. Python takes a `LettaAdapterConfig` dataclass; TypeScript takes a flat `LettaAdapterOptions` object. #### Python `LettaAdapter(config=None, history_converter=None, **features)` `LettaAdapterConfig` fields: | Field | Type | Default | Purpose | | ------------------------------- | ------------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `agent_id` | `str \| None` | `None` | Bootstrap hint for the lone agent in `shared` mode. Ignored in `per_room` mode, which resumes only from room history | | `model` | `str \| None` | `None` | Letta model handle, provider prefix required | | `provider_key` | `str \| None` | `None` | Letta API key. Required for Letta Cloud, optional self-hosted | | `base_url` | `str` | `"https://api.letta.com"` | Letta server URL | | `project` | `str \| None` | `None` | Letta Cloud project scope, ignored self-hosted | | `embedding` | `str \| None` | `None` | Embedding model on agent create. Required by Letta's Docker server, Cloud picks its own default | | `custom_section` | `str` | `""` | Extra instructions appended to the rendered system prompt | | `include_base_instructions` | `bool` | `True` | Include Band's base instructions in the system prompt | | `persona` | `str \| None` | `None` | Replaces the rendered system prompt in the persona memory block | | `memory_blocks` | `list[dict[str, str]]` | `[]` | Extra memory blocks on agent create. The persona block is inserted ahead of them | | `mode` | `"per_room" \| "shared"` | `"per_room"` | One Letta agent per room, or one agent with a per-room Conversation | | `mcp` | `LettaMCPConfig` | `LettaMCPConfig()` | How Letta reaches Band's tools | | `auto_relay` | `bool` | `True` | Relay assistant text when the agent skipped the send tool. Set `False` to fail loudly instead | | `turn_timeout_s` | `float` | `300.0` | Per-turn timeout. On expiry the adapter reports an error event | | `summary_max_length` | `int` | `150` | Character budget for the stored topic hint used in rejoin notes | | `consolidate_memory_on_cleanup` | `bool` | `True` | Send a consolidation prompt on room cleanup (`per_room` only). Skipped when `delete_agents_on_cleanup` is on, since the agent is deleted instead | | `delete_agents_on_cleanup` | `bool` | `False` | Delete the room's Letta agent on cleanup (`per_room` only) | | `teardown_timeout_s` | `float` | `10.0` | Upper bound for best-effort teardown calls | `LettaAdapterConfig` rejects unknown field names, so a typo fails construction rather than vanishing. The removed `enable_task_events`, `enable_memory_tools`, and `enable_execution_reporting` booleans are unknown names now: pass `emit` and `capabilities` to the adapter instead. Most fields also read a `LETTA_`-prefixed environment variable, for example `LETTA_BASE_URL`, `LETTA_MODEL`, and `LETTA_EMBEDDING`. `provider_key` additionally accepts `LETTA_API_KEY`, matching Letta Cloud's own naming. An explicit constructor argument always wins over the environment. `LettaMCPConfig` fields: | Field | Type | Default | Purpose | | ----------------- | ---------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `mode` | `"self_host" \| "external"` | `"self_host"` | Self-host the Band MCP server in-process, or register an external one | | `server_url` | `str` | `"http://localhost:8002/sse"` | External mode only, URL of the running Band MCP server. Letta refuses the `localhost` default | | `server_name` | `str \| None` | `None` | Registration name in Letta. Resolves to `band` in external mode, and to a fresh `band-{suffix}` per registration when self-hosted | | `bind_host` | `str` | `"127.0.0.1"` | Interface the in-process server binds. `"0.0.0.0"` exposes your agent's tools to the local network | | `advertised_host` | `str \| None` | `None` | Hostname Letta uses to reach the local server. Defaults to `bind_host`, except a wildcard bind (`"0.0.0.0"` or `"::"`) falls back to `127.0.0.1`. Letta refuses every private address, so a dockerized Letta on your laptop cannot be reached, see [Letta Server Connection](#letta-server-connection) | | `transport` | `"sse" \| "streamable_http"` | `"sse"` | MCP transport | Capabilities and event emission are keyword arguments on the adapter, not fields on the config: ```python from band import Capability, Emit adapter = LettaAdapter( config=LettaAdapterConfig(model="openai/gpt-4o"), capabilities={Capability.MEMORY, Capability.CONTACTS}, emit={Emit.TOOL_CALLS, Emit.TASK_EVENTS, Emit.USAGE}, ) ``` Supported capabilities are `MEMORY` and `CONTACTS`, both opt-in. Supported emissions are `TOOL_CALLS`, `TASK_EVENTS`, and `USAGE`, and omitting `emit` resolves to all three, so the example above is the default spelled out. `emit=()` silences the adapter; naming any other `Emit` member raises `BandConfigError` at construction. Token usage is only available on the `per_room` path, since the shared-mode Conversations stream carries no aggregate usage. > **Warning** > > `Emit.TASK_EVENTS` is load-bearing here, not narration: `letta_agent_id` is recorded in task-event metadata and read back to resume the server-side agent. Narrowing `emit` so it excludes `Emit.TASK_EVENTS` means every restart creates a fresh Letta agent instead of reattaching. > **Warning** > > `api_key`, `mcp_server_url`, and `mcp_server_name` are deprecated on `LettaAdapterConfig` and emit `DeprecationWarning`. Use `provider_key` and `mcp=LettaMCPConfig(...)`. Passing both `api_key` and `provider_key` raises `BandConfigError`. #### TypeScript `new LettaAdapter(options: LettaAdapterOptions = {})` | Option | Type | Default | Purpose | | ------------------------- | --------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------- | | `model` | `string` | `"openai/gpt-4o"` | Letta model handle used on agent create | | `lettaApiKey` | `string` | unset | Passed to the Letta client as `apiKey` | | `lettaBaseUrl` | `string` | unset | Passed to the Letta client as `baseURL`. Unset uses the client's own default | | `lettaAgentId` | `string` | unset | Use one existing Letta agent for every room. Disables per-room creation, history injection, and cleanup deletion | | `embedding` | `string` | unset | Embedding model, sent on agent create only when set | | `memoryBlocks` | `Array<{ label, value }>` | `[]` | Memory blocks on agent create | | `serverTools` | `string[]` | `[]` | Names of Letta server-side tools to attach on agent create | | `includeBaseTools` | `boolean` | `false` | Letta's `include_base_tools` on agent create | | `contextWindowLimit` | `number` | unset | Letta's `context_window_limit` on agent create | | `maxToolRounds` | `number` | `8` | Approval and tool-return rounds per turn, clamped to at least 1 | | `responseTimeoutSeconds` | `number` | `120` | Wall-clock budget covering Letta API time and local tool execution, clamped to at least 1 | | `systemPrompt` | `string` | unset | Replaces the rendered system prompt entirely | | `customSection` | `string` | unset | Extra instructions in the rendered system prompt | | `includeBaseInstructions` | `boolean` | `true` | Include Band's base instructions | | `maxHistoryMessages` | `number` | `100` | Cap on injected bootstrap history entries, applied after incomplete exchanges are filtered out | | `emitReasoningEvents` | `boolean` | `false` | Emit Letta reasoning as `thought` events | | `historyConverter` | `LettaHistoryConverter` | new instance | Override history conversion | | `clientFactory` | `() => Promise` | unset | Supply the Letta client yourself instead of importing `@letta-ai/letta-client` | | `logger` | `Logger` from `@band-ai/sdk/core` | no-op logger | Adapter logging. Use the exported `ConsoleLogger`, which redacts secret-looking keys in log context | ### Differences from the Python SDK | Capability | Python | TypeScript | | ----------------------------------- | ---------------------------------------------- | --------------------------------------------- | | Platform tool transport | Band MCP server registered with Letta | Letta `client_tools`, executed locally | | `shared` mode via Conversations API | Yes | No, a shared `lettaAgentId` only | | Memory and contacts capabilities | `capabilities` | Not configurable | | Token usage events | `Emit.USAGE`, on by default | Not emitted | | Reasoning events | Not emitted | `emitReasoningEvents` | | Auto-relay of untooled text | `auto_relay`, on by default | Always sends the assistant text | | Agent deletion on room cleanup | Opt in with `delete_agents_on_cleanup` | Default for per-room agents | | Memory consolidation on cleanup | `consolidate_memory_on_cleanup`, on by default | Not implemented | | History seeding | Inline `[System]:` lines in the next turn | Separate injected message with `max_steps: 1` | | Per-turn tool round cap | Handled by Letta server-side | `maxToolRounds` | --- ## Letta Server Connection #### Python Three fields configure the connection. Each also reads a `LETTA_`-prefixed environment variable, so wire your own settings in explicitly when you do not want that fallback. | Target | Fields | | ----------- | ------------------------------------------------------------------------------------------------------- | | Letta Cloud | `provider_key` (required), `project` (optional). `base_url` already defaults to `https://api.letta.com` | | Self-hosted | `base_url="http://localhost:8283"`, no `provider_key` needed, `embedding` required by the Docker server | Agent identity is not configured for `per_room` mode. The adapter records `letta_agent_id` in a task event and the history converter reads it back on the next session, so restarts resume the same Letta agent. Passing `agent_id` only takes effect in `shared` mode. To run a self-hosted Letta server, start it detached, with a persistent volume and at least one model provider key: ```bash docker run -d --name letta \ -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \ -p 8283:8283 \ -e OPENAI_API_KEY="$OPENAI_API_KEY" \ -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \ letta/letta:latest ``` Letta syncs each provider's model list at startup, so a server started without a provider key exposes only the `letta/letta-free` handle and `model="anthropic/claude-haiku-4-5"` does not resolve. Add the key, restart the container, then check what the server exposes with `curl -fsS http://localhost:8283/v1/models/`. Without the volume, agents and MCP registrations are lost when the container is removed. ### Reaching Band's tools MCP is the Python SDK's only platform-tool transport: the Letta server calls back into a Band MCP server over HTTP. Letta validates that URL against its own SSRF guard, `letta/helpers/url_validation.py`, which rejects `localhost`, any literal private IP, and any hostname that resolves to a non-global IP. The Band MCP server therefore has to sit on a publicly resolvable host, whichever Letta you point at. Two configurations work today: | Letta | MCP config | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | Letta Cloud | `mode="external"`, `server_url` on a public HTTPS host | | Self-hosted on a publicly reachable host | `mode="external"` with that host's public URL, or `mode="self_host"` with `advertised_host` set to its public name | ```python mcp=LettaMCPConfig( mode="external", server_url="https://your-band-mcp.example.com/sse", ) ``` The defaults reach nothing. `LettaMCPConfig()` self-hosts on `127.0.0.1`, and Letta rejects that registration with `422 Non-public IP not allowed: 127.0.0.1`, so `on_started` raises and the agent never starts. The `server_url` default, `http://localhost:8002/sse`, is refused the same way as `Blocked internal hostname: localhost`. > **Warning** > > A Letta server running in Docker on your laptop cannot use the Python tool path. `advertised_host="host.docker.internal"` clears registration, because the request schema calls the validator with `resolve_hostname=False`, but the tool sync that follows does resolve the hostname: > > ``` > Letta.letta.services.mcp_server_manager - WARNING - Error listing tools for MCP server mcp_server-...: Hostname resolves to non-public IP: 0.250.250.254 > Letta.letta.services.mcp_server_manager - WARNING - Failed to auto-sync tools from MCP server band-...: Hostname resolves to non-public IP: 0.250.250.254. Server was created successfully but tools were not persisted. > ``` > > The adapter surfaces this as a warning plus `Discovered 0 MCP tools: []` and keeps running, so the symptom is an agent with no send, participant or room tools that answers through `auto_relay` text only. Both `transport="sse"` and `transport="streamable_http"` fail identically, and no environment variable, flag or allowlist disables the guard. No value of `bind_host` or `advertised_host` helps, because every address that reaches your laptop is private. To develop against a local Letta server, expose the Band MCP server through a public tunnel and register the tunnel URL with `mode="external"`. > **Tip** > > If Letta answers `INVALID_ARGUMENT: The model handle should be in the format provider/model-name`, your `model` is missing its provider prefix. List the handles your server exposes with `curl -fsS http://localhost:8283/v1/models/`. #### TypeScript `lettaApiKey` and `lettaBaseUrl` are forwarded to the `@letta-ai/letta-client` constructor as `apiKey` and `baseURL`. Each is omitted when unset, so the client falls back to its own defaults. The adapter reads no environment variables, so pass values in explicitly. ```typescript // Letta Cloud new LettaAdapter({ lettaApiKey: process.env.LETTA_API_KEY }); // Self-hosted new LettaAdapter({ lettaBaseUrl: "http://localhost:8283", embedding: "openai/text-embedding-3-small", }); ``` Agent identity has two modes: * **Per room (default)** - one Letta agent per room, created as `band-room-` and deleted on room cleanup. * **Shared** - set `lettaAgentId` to an existing Letta agent. Every room uses it, bootstrap history injection is skipped, and cleanup never deletes it. To connect through a client you construct yourself, for example one with custom transport settings, pass `clientFactory` and skip the built-in import: ```typescript new LettaAdapter({ clientFactory: async () => myLettaClient, }); ``` --- ## Complete Example #### Python Letta Cloud, an external Band MCP server on a public host, memory and contacts capabilities on, execution and usage events emitted, and a custom memory block: **`agent.py`** ```python title="agent.py" import asyncio import logging import os from dotenv import load_dotenv from band import Agent, Capability, Emit from band.adapters.letta import LettaAdapter, LettaAdapterConfig, LettaMCPConfig from band.config import load_agent_config logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) async def main(): load_dotenv() agent_id, api_key = load_agent_config("my_agent") adapter = LettaAdapter( config=LettaAdapterConfig( provider_key=os.getenv("LETTA_API_KEY"), model="anthropic/claude-haiku-4-5", custom_section="You are a project assistant. Track decisions and owners.", memory_blocks=[ { "label": "project", "value": "Current project: Q1 platform launch.", }, ], mcp=LettaMCPConfig( mode="external", server_url=os.getenv( "BAND_MCP_URL", "https://your-band-mcp.example.com/sse" ), ), turn_timeout_s=180.0, ), capabilities={Capability.MEMORY, Capability.CONTACTS}, emit={Emit.TOOL_CALLS, Emit.TASK_EVENTS, Emit.USAGE}, ) agent = Agent.create( adapter=adapter, agent_id=agent_id, api_key=api_key, ws_url=os.getenv("BAND_WS_URL", "wss://app.band.ai/api/v1/socket/websocket"), rest_url=os.getenv("BAND_REST_URL", "https://app.band.ai"), ) logger.info("Letta agent is running! Press Ctrl+C to stop.") await agent.run() if __name__ == "__main__": asyncio.run(main()) ``` #### TypeScript Per-room Letta agents against Letta Cloud, with reasoning events, a custom memory block, and a tighter turn budget: **`agent.ts`** ```typescript title="agent.ts" import { Agent, LettaAdapter, loadAgentConfig } from "@band-ai/sdk"; import { ConsoleLogger } from "@band-ai/sdk/core"; const adapter = new LettaAdapter({ model: "anthropic/claude-haiku-4-5", lettaApiKey: process.env.LETTA_API_KEY, embedding: "openai/text-embedding-3-small", customSection: "You are a project assistant. Track decisions and owners.", memoryBlocks: [ { label: "project", value: "Current project: Q1 platform launch." }, ], includeBaseTools: true, maxToolRounds: 6, responseTimeoutSeconds: 90, maxHistoryMessages: 40, emitReasoningEvents: true, logger: new ConsoleLogger(), }); const agent = Agent.create({ adapter, config: loadAgentConfig("my_agent"), wsUrl: process.env.BAND_WS_URL, restUrl: process.env.BAND_REST_URL, }); console.log("Letta agent is running! Press Ctrl+C to stop."); await agent.run(); ``` --- ## Next Steps #### [Setup](/integrations/sdks/tutorials/setup) Install the SDK and configure credentials #### [Agent Lifecycle](/integrations/sdks/tutorials/agent-lifecycle) Room subscriptions, sessions, and cleanup #### [Testing Agents](/integrations/sdks/tutorials/testing-agents) Test adapters without a live platform connection #### [Custom Adapters](/integrations/sdks/tutorials/creating-framework-integrations) Build adapters for any agent framework > Connect a stateful Letta agent to Band with the Python or TypeScript SDK