{
"path": "/agents-sdk",
"site": "at://did:plc:gfrmhdmjvxn2sjedzboeudef/site.standard.publication/3md7ylshxzk2y",
"$type": "site.standard.document",
"title": "A quick comparison between Letta and the Claude Agent SDK",
"content": {
"$type": "site.standard.document#markdown",
"value": "# A quick comparison between Letta and the Claude Agent SDK\n\nI spent a couple hours with Anthropic's new [Claude Agent SDK](https://docs.claude.com/en/api/agent-sdk/overview) to see how it compares to Letta. It's the closest thing to what we're building.\n\nTL;DR: It's basically Claude Code's internals exposed as an SDK. But it's a subset of what Letta offers, and the two systems have fundamentally different design philosophies.\n\n## Memory\n\nThe big differentiator in my view is how memory works. In Letta, memory is a first-class citizen. Letta agents are stateful by default through the use of specialized sections of the context window called [memory blocks](https://docs.letta.com/guides/agents/memory-blocks). Memory blocks are editable by the agent, which gives it the ability to self-improve and adapt to its environment. Memory blocks are also maintained server-side in a database, rather than in files.\n\nClaude uses Anthropic's standard brute-force approach to memory. There's a `CLAUDE.md` file that loads into the agent's window, and compaction happens when the context window is exceeded.\n\nLetta agents use smaller but regular compactions. Agents carry forward meaningful information by inserting it into memory blocks. This allows agents to grab information as it arises and becomes meaningful, rather than attempting to extract it at the end of the context window.\n\nIn the Agent SDK, you load memory files through `setting_sources`:\n\n```python\n# Load project settings to include CLAUDE.md files\nasync for message in query(\n prompt=\"Add a new feature following project conventions\",\n options=ClaudeAgentOptions(\n system_prompt={\n \"type\": \"preset\",\n \"preset\": \"claude_code\"\n },\n setting_sources=[\"project\"], # Required to load CLAUDE.md from project\n allowed_tools=[\"Read\", \"Write\", \"Edit\"]\n )\n):\n print(message)\n```\n\nClaude waits until the notebook is full, then summarizes everything at once. Letta takes notes continuously as important things happen. This enables a fundamentally different kind of agent—one that evolves its own knowledge base rather than just maintaining conversation history.\n\n## Client-Side vs. Server-Side Architecture\n\nThe biggest architectural difference: **Claude Agent SDK is client-side first, Letta is server-side first.**\n\nClient-side means your agent runs in your application's process. When your script ends, the agent disappears. State lives in memory or local files.\n\nServer-side means your agent lives on a server. It's always there, always maintaining state. Multiple applications can talk to the same agent. When your script ends, the agent keeps running.\n\n### Why This Matters\n\nThe persistence model is completely different. With Claude Agent SDK, you instantiate an agent for each interaction. The agent exists for the duration of your script, then it's gone. If you want to remember something from a previous session, you need to manage that state yourself.\n\n```python\n# Session 1\nasync for message in query(prompt=\"My name is Cameron\"):\n print(message)\n# Agent forgets everything when this script ends\n\n# Session 2 (later)\nasync for message in query(prompt=\"What's my name?\"):\n print(message) # Agent has no idea\n```\n\nWith Letta, agents are persistent by default. You create an agent once, and it lives on the server. Every interaction adds to its history and memory. You can message it from your laptop today, your phone tomorrow, and a cron job next week—it's always the same agent with the same memory.\n\n```python\n# Session 1\nclient.agents.messages.create(\n agent_id=\"agent-123\",\n messages=[{\"role\": \"user\", \"content\": \"My name is Cameron\"}]\n)\n\n# Session 2 (days later, different machine)\nresponse = client.agents.messages.create(\n agent_id=\"agent-123\",\n messages=[{\"role\": \"user\", \"content\": \"What's my name?\"}]\n)\n# Agent remembers: \"Cameron\"\n```\n\nThe agent's ID is the key to its entire history. This architectural choice enables use cases that are difficult with ephemeral agents: monitoring systems that run on schedules, agents that respond to webhooks from external services, bots that maintain consistent personality across multiple platforms like Slack and Discord, and agents that coordinate with other agents in a shared environment.\n\nClaude Agent SDK is optimized for personal productivity tools, prototyping, and scenarios where you need direct filesystem access on your machine and your agent's lifecycle matches your script's lifecycle. Letta is optimized for production applications where you need persistent stateful agents, multiple clients talking to the same agent, agents that self-evolve over time, and agents accessible via API from anywhere.\n\n## First Impressions\n\nClaude Agent SDK ships with TypeScript and Python support, same as Letta. It includes auto-compaction when the context window fills up. Letta does this too, though Claude's approach is simpler since they don't track persistent state the way we do. Both systems have first-class MCP integration and agent permissions for controlling tool access.\n\nThe SDK uses a `.claude` directory structure similar to Claude Code. There are subagents for specialized prompts, hooks for custom commands that run at specific events, slash commands for common operations, and memory stored in `CLAUDE.md` files. Tool permissions are controlled with `allowedTools`, `disallowedTools`, and `permissionMode` parameters.\n\n## Quick Start Examples\n\nHere's the same task in both systems: asking an agent to create a Python web server. These aren't exactly equivalent due to the server/client difference and which tools are available, but this should give you a rough sketch of what they look like.\n\n**Claude Agent SDK:**\n\n```python\nfrom dotenv import load_dotenv\nimport os\nimport asyncio\nfrom claude_agent_sdk import query, ClaudeAgentOptions\n\nload_dotenv()\n\nasync def main():\n options = ClaudeAgentOptions(\n system_prompt=\"You are an expert Python developer\",\n permission_mode='acceptEdits',\n cwd=\"/users/Cameron/letta/agent-sdk/sandbox\"\n )\n\n async for message in query(\n prompt=\"Create a Python web server\",\n options=options\n ):\n print(message)\n\nasyncio.run(main())\n```\n\n**Letta (first time):**\n\n```python\nfrom letta_client import Letta\nimport os\n\nclient = Letta(token=os.getenv(\"LETTA_API_KEY\"))\n\n# Create the agent once\nagent = client.agents.create(\n model=\"openai/gpt-4o-mini\",\n embedding=\"openai/text-embedding-3-small\",\n memory_blocks=[\n {\"label\": \"persona\", \"value\": \"You are an expert Python developer.\"}\n ]\n)\n\n# Send a message\nresponse = client.agents.messages.create(\n agent_id=agent.id,\n messages=[{\"role\": \"user\", \"content\": \"Create a Python web server\"}]\n)\n```\n\n**Letta (every subsequent time):**\n\n```python\nfrom letta_client import Letta\nimport os\n\nclient = Letta(token=os.getenv(\"LETTA_API_KEY\"))\n\n# Just send a message to your existing agent\nresponse = client.agents.messages.create(\n agent_id=\"agent-123\",\n messages=[{\"role\": \"user\", \"content\": \"Create a Python web server\"}]\n)\n```\n\nThe Claude example is simpler for one-off queries. The Letta example requires agent creation the first time, but after that it's just one API call. The agent persists on the server, maintains its memory and context, and is accessible from anywhere.\n\nRunning local tools client-side in Letta is something we're actively working on.\n\n## Tools: Two Philosophies\n\n### Claude's Approach\n\nClaude's tool definition syntax is clean:\n\n```python\nfrom claude_agent_sdk import tool\nfrom typing import Any\n\n@tool(\"greet\", \"Greet a user\", {\"name\": str})\nasync def greet(args: dict[str, Any]) -> dict[str, Any]:\n return {\n \"content\": [{\n \"type\": \"text\",\n \"text\": f\"Hello, {args['name']}!\"\n }]\n }\n```\n\nThe `@tool` decorator wraps your function into a standard MCP tool. You can spin up an MCP server quickly:\n\n```python\ncalculator = create_sdk_mcp_server(\n name=\"howdy_server\",\n version=\"2.0.0\",\n tools=[greet] # Pass decorated functions\n)\n```\n\nThen expose it to your agent:\n\n```python\noptions = ClaudeAgentOptions(\n mcp_servers={\"howdy\": calculator},\n allowed_tools=[\"mcp__howdy__greet\"]\n)\n```\n\n### Letta's Approach\n\nLetta supports two methods for tools: persistent custom tools executed in a sandbox, and MCP servers (same as Anthropic).\n\nLetta's built-in approach is designed for persistent, server-registered tools shared across all agents:\n\n```python\nfrom letta_client import Letta\n\nclient = Letta(token=os.getenv(\"LETTA_API_KEY\"))\n\n# Define your function with Google Style docstring\ndef greet(name: str) -> str:\n \"\"\"\n Greet a user by name.\n\n Args:\n name (str): The name of the user to greet\n\n Returns:\n str: A greeting message\n \"\"\"\n return f\"Hello, {name}!\"\n\n# Create the tool from the function\ntool = client.tools.create_from_function(func=greet)\n\n# Create an agent with the tool attached\nagent = client.agents.create(\n model=\"openai/gpt-4o-mini\",\n embedding=\"openai/text-embedding-3-small\",\n memory_blocks=[\n {\"label\": \"persona\", \"value\": \"I am a helpful assistant.\"}\n ],\n tools=[\"greet\"] # Attach tool by name\n)\n\n# The tool can also be attached directly using the ID\nclient.agents.tools.attach(\n agent_id=agent.id,\n tool_id=tool.id\n)\n\n# Use the agent (it will call the greet tool)\nresponse = client.agents.messages.create(\n agent_id=agent.id,\n messages=[{\"role\": \"user\", \"content\": \"Please greet Cameron\"}]\n)\n```\n\n[Letta also supports](https://docs.letta.com/guides/mcp/overview) Anthropic's MCP approach directly. For local servers, here's a stdio example:\n\n```python\nfrom letta_client import Letta\nfrom letta_client.types import StdioServerConfig\n\n# Self-hosted only\nclient = Letta(base_url=\"http://localhost:8283\")\n\n# Connect a stdio server (npx example - works in Docker!)\nstdio_config = StdioServerConfig(\n server_name=\"github-server\",\n command=\"npx\",\n args=[\"-y\", \"@modelcontextprotocol/server-github\"],\n env={\"GITHUB_PERSONAL_ACCESS_TOKEN\": \"your-token\"}\n)\nclient.tools.add_mcp_server(request=stdio_config)\n\n# List available tools\ntools = client.tools.list_mcp_tools_by_server(\n mcp_server_name=\"github-server\"\n)\n\n# Add a tool to use with agents\ntool = client.tools.add_mcp_tool(\n mcp_server_name=\"github-server\",\n mcp_tool_name=\"create_repository\"\n)\n```\n\nBoth approaches work well. They're roughly equivalent, though Letta supports server-side code execution and will soon support client-side execution as well.\n\n## Hooks vs. Tool Rules\n\nClaude Agent SDK has [hooks](https://docs.claude.com/en/docs/claude-code/hooks-guide#quickstart) for executing code around specific events:\n\n```python\nHookEvent = Literal[\n \"PreToolUse\", # Called before tool execution\n \"PostToolUse\", # Called after tool execution\n \"UserPromptSubmit\", # Called when user submits a prompt\n \"Stop\", # Called when stopping execution\n \"SubagentStop\", # Called when a subagent stops\n \"PreCompact\" # Called before message compaction\n]\n```\n\nLetta's closest equivalent is [tool rules](https://docs.letta.com/guides/agents/tool-rules), which force agents to call tools in specific orders. Claude's hook approach allows for arbitrary code execution outside the model. Letta's tools can approximate hook behavior, and tool rules give you fine-grained control over agent workflows.\n\nThat said, the Agent SDK approach to hooks is quite powerful and well-suited to the kind of tight filesystem integration that Claude Code benefits from.\n\n## Conclusion\n\nClaude Agent SDK is well-executed, as you might imagine from Anthropic. It's a subset of what Letta offers, optimized for different use cases.\n\nClaude Agent SDK is designed for low-friction client-side interaction, ephemeral agents that spin up and down quickly, direct code execution in your local environment, and quick prototypes and one-off tasks.\n\nLetta is designed for persistent server-side agents, globally accessible agents that maintain state, self-evolving agents that manage their own memory, and production deployments with multiple agents coordinating.\n\nIf you need an agent to help you write code on your laptop for an hour, Claude Agent SDK works. If you need an agent that remembers your last 50 conversations, coordinates with other agents, and improves itself over time—that's what Letta is built for.\n\nTry Letta at [app.letta.com](https://app.letta.com) or check out the [documentation](https://docs.letta.com).\n\n-- Cameron"
},
"description": "I spent a few hours with Anthropic's Agent SDK and compared it to Letta. Not bad -- different architectures, but so far the Agent SDK is the closest to what Letta is designed for.",
"publishedAt": "2025-10-10T07:00:00.000Z",
"textContent": "A quick comparison between Letta and the Claude Agent SDK\n\nI spent a couple hours with Anthropic's new Claude Agent SDK to see how it compares to Letta. It's the closest thing to what we're building.\n\nTL;DR: It's basically Claude Code's internals exposed as an SDK. But it's a subset of what Letta offers, and the two systems have fundamentally different design philosophies.\n\nMemory\n\nThe big differentiator in my view is how memory works. In Letta, memory is a first-class citizen. Letta agents are stateful by default through the use of specialized sections of the context window called memory blocks. Memory blocks are editable by the agent, which gives it the ability to self-improve and adapt to its environment. Memory blocks are also maintained server-side in a database, rather than in files.\n\nClaude uses Anthropic's standard brute-force approach to memory. There's a file that loads into the agent's window, and compaction happens when the context window is exceeded.\n\nLetta agents use smaller but regular compactions. Agents carry forward meaningful information by inserting it into memory blocks. This allows agents to grab information as it arises and becomes meaningful, rather than attempting to extract it at the end of the context window.\n\nIn the Agent SDK, you load memory files through :\n\nClaude waits until the notebook is full, then summarizes everything at once. Letta takes notes continuously as important things happen. This enables a fundamentally different kind of agent—one that evolves its own knowledge base rather than just maintaining conversation history.\n\nClient-Side vs. Server-Side Architecture\n\nThe biggest architectural difference: Claude Agent SDK is client-side first, Letta is server-side first.\n\nClient-side means your agent runs in your application's process. When your script ends, the agent disappears. State lives in memory or local files.\n\nServer-side means your agent lives on a server. It's always there, always maintaining state. Multiple applications can talk to the same agent. When your script ends, the agent keeps running.\n\nWhy This Matters\n\nThe persistence model is completely different. With Claude Agent SDK, you instantiate an agent for each interaction. The agent exists for the duration of your script, then it's gone. If you want to remember something from a previous session, you need to manage that state yourself.\n\nWith Letta, agents are persistent by default. You create an agent once, and it lives on the server. Every interaction adds to its history and memory. You can message it from your laptop today, your phone tomorrow, and a cron job next week—it's always the same agent with the same memory.\n\nThe agent's ID is the key to its entire history. This architectural choice enables use cases that are difficult with ephemeral agents: monitoring systems that run on schedules, agents that respond to webhooks from external services, bots that maintain consistent personality across multiple platforms like Slack and Discord, and agents that coordinate with other agents in a shared environment.\n\nClaude Agent SDK is optimized for personal productivity tools, prototyping, and scenarios where you need direct filesystem access on your machine and your agent's lifecycle matches your script's lifecycle. Letta is optimized for production applications where you need persistent stateful agents, multiple clients talking to the same agent, agents that self-evolve over time, and agents accessible via API from anywhere.\n\nFirst Impressions\n\nClaude Agent SDK ships with TypeScript and Python support, same as Letta. It includes auto-compaction when the context window fills up. Letta does this too, though Claude's approach is simpler since they don't track persistent state the way we do. Both systems have first-class MCP integration and agent permissions for controlling tool access.\n\nThe SDK uses a directory structure similar to Claude Code. There are subagents for specialized prompts, hooks for custom commands that run at specific events, slash commands for common operations, and memory stored in files. Tool permissions are controlled with , , and parameters.\n\nQuick Start Examples\n\nHere's the same task in both systems: asking an agent to create a Python web server. These aren't exactly equivalent due to the server/client difference and which tools are available, but this should give you a rough sketch of what they look like.\n\nClaude Agent SDK:\n\nLetta (first time):\n\nLetta (every subsequent time):\n\nThe Claude example is simpler for one-off queries. The Letta example requires agent creation the first time, but after that it's just one API call. The agent persists on the server, maintains its memory and context, and is accessible from anywhere.\n\nRunning local tools client-side in Letta is something we're actively working on.\n\nTools: Two Philosophies\n\nClaude's Approach\n\nClaude's tool definition syntax is clean:\n\nThe decorator wraps your function into a standard MCP tool. You can spin up an MCP server quickly:\n\nThen expose it to your agent:\n\nLetta's Approach\n\nLetta supports two methods for tools: persistent custom tools executed in a sandbox, and MCP servers (same as Anthropic).\n\nLetta's built-in approach is designed for persistent, server-registered tools shared across all agents:\n\nLetta also supports Anthropic's MCP approach directly. For local servers, here's a stdio example:\n\nBoth approaches work well. They're roughly equivalent, though Letta supports server-side code execution and will soon support client-side execution as well.\n\nHooks vs. Tool Rules\n\nClaude Agent SDK has hooks for executing code around specific events:\n\nLetta's closest equivalent is tool rules, which force agents to call tools in specific orders. Claude's hook approach allows for arbitrary code execution outside the model. Letta's tools can approximate hook behavior, and tool rules give you fine-grained control over agent workflows.\n\nThat said, the Agent SDK approach to hooks is quite powerful and well-suited to the kind of tight filesystem integration that Claude Code benefits from.\n\nConclusion\n\nClaude Agent SDK is well-executed, as you might imagine from Anthropic. It's a subset of what Letta offers, optimized for different use cases.\n\nClaude Agent SDK is designed for low-friction client-side interaction, ephemeral agents that spin up and down quickly, direct code execution in your local environment, and quick prototypes and one-off tasks.\n\nLetta is designed for persistent server-side agents, globally accessible agents that maintain state, self-evolving agents that manage their own memory, and production deployments with multiple agents coordinating.\n\nIf you need an agent to help you write code on your laptop for an hour, Claude Agent SDK works. If you need an agent that remembers your last 50 conversations, coordinates with other agents, and improves itself over time—that's what Letta is built for.\n\nTry Letta at app.letta.com or check out the documentation.\n\n-- Cameron"
}