diff --git a/fern/assets/leaderboard.js b/fern/assets/leaderboard.js
deleted file mode 100644
index f627ce6c..00000000
--- a/fern/assets/leaderboard.js
+++ /dev/null
@@ -1,153 +0,0 @@
-/* ──────────────────────────────────────────────────────────
- assets/leaderboard.js
- Load via docs.yml → js: - path: assets/leaderboard.js
- (strategy: lazyOnload is fine)
- ────────────────────────────────────────────────────────── */
-
-import yaml from 'https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/+esm';
-
-console.log('🏁 leaderboard.js loaded on', location.pathname);
-
-const COST_CAP = 120;
-
-/* ---------- helpers ---------- */
-const pct = (v) => Number(v).toPrecision(3) + '%';
-const cost = (v) => '$' + Number(v).toFixed(2);
-const ready = (cb) =>
- document.readyState === 'loading'
- ? document.addEventListener('DOMContentLoaded', cb)
- : cb();
-
-/* ---------- main ---------- */
-ready(async () => {
- // const host = document.getElementById('letta-leaderboard');
- // if (!host) {
- // console.warn('LB-script: #letta-leaderboard not found - bailing out.');
- // return;
- // }
- /* ---- wait for the leaderboard container to appear (SPA nav safe) ---- */
- const host = await new Promise((resolve, reject) => {
- const el = document.getElementById('letta-leaderboard');
- if (el) return resolve(el); // SSR / hard refresh path
-
- const obs = new MutationObserver(() => {
- const found = document.getElementById('letta-leaderboard');
- if (found) {
- obs.disconnect();
- resolve(found); // CSR navigation path
- }
- });
- obs.observe(document.body, { childList: true, subtree: true });
-
- setTimeout(() => {
- obs.disconnect();
- reject(new Error('#letta-leaderboard never appeared'));
- }, 5000); // safety timeout
- }).catch((err) => {
- console.warn('LB-script:', err.message);
- return null;
- });
- if (!host) return; // still no luck → give up
-
- /* ----- figure out URL of data.yaml ----- */
- // const path = location.pathname.endsWith('/')
- // ? location.pathname
- // : location.pathname.replace(/[^/]*$/, ''); // strip file/slug
- // const dataUrl = `${location.origin}${path}data.yaml`;
- // const dataUrl = `${location.origin}/leaderboard/data.yaml`; // one-liner, always right
- // const dataUrl = `${location.origin}/assets/leaderboard.yaml`;
- // const dataUrl = `./assets/leaderboard.yaml`; // one-liner, always right
- // const dataUrl = `${location.origin}/data.yaml`; // one-liner, always right
- const dataUrl =
- 'https://raw.githubusercontent.com/letta-ai/letta-evals/refs/heads/main/letta-leaderboard/leaderboard_results.yaml';
- // const dataUrl = 'https://cdn.jsdelivr.net/gh/letta-ai/letta-evals@latest/letta-leaderboard/leaderboard_results.yaml';
-
- console.log('LB-script: fetching', dataUrl);
-
- /* ----- fetch & parse YAML ----- */
- let rows;
- try {
- const resp = await fetch(dataUrl);
- console.log(`LB-script: status ${resp.status}`);
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- rows = yaml.load(await resp.text());
- } catch (err) {
- console.error('LB-script: failed to load YAML →', err);
- return;
- }
-
- /* ----- wire up table ----- */
- const dir = Object.create(null);
- const tbody = document.getElementById('lb-body');
- const searchI = document.getElementById('lb-search');
- const headers = document.querySelectorAll('#lb-table thead th[data-key]');
- searchI.value = ''; // clear any persisted filter
-
- const render = () => {
- const q = searchI.value.toLowerCase();
- tbody.innerHTML = rows
- .map((r) => {
- const over = r.total_cost > COST_CAP;
- const barW = over ? '100%' : (r.total_cost / COST_CAP) * 100 + '%';
- const costCls = over ? 'cost-high' : 'cost-ok';
- const warnIcon = over
- ? `⚠`
- : '';
-
- return `
-
- | ${r.model} |
-
-
-
- ${pct(r.average)}
- |
-
-
-
- ${cost(r.total_cost)}
- ${warnIcon}
- |
-
`;
- })
- .join('');
- };
-
- const setIndicator = (activeKey) => {
- headers.forEach((h) => {
- h.classList.remove('asc', 'desc');
- if (h.dataset.key === activeKey) h.classList.add(dir[activeKey]);
- });
- };
-
- /* initial sort ↓ */
- dir.average = 'desc';
- rows.sort((a, b) => b.average - a.average);
- setIndicator('average');
- render();
-
- /* search */
- searchI.addEventListener('input', render);
-
- /* column sorting */
- headers.forEach((th) => {
- const key = th.dataset.key;
- th.addEventListener('click', () => {
- const asc = dir[key] === 'desc';
- dir[key] = asc ? 'asc' : 'desc';
-
- rows.sort((a, b) => {
- const va = a[key],
- vb = b[key];
- const cmp =
- typeof va === 'number'
- ? va - vb
- : String(va).localeCompare(String(vb));
- return asc ? cmp : -cmp;
- });
-
- setIndicator(key);
- render();
- });
- });
-});
diff --git a/fern/examples/agent_config.py b/fern/examples/agent_config.py
deleted file mode 100644
index 84cb9779..00000000
--- a/fern/examples/agent_config.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from letta_client import Letta
-
-client = Letta(base_url="http://localhost:8283")
-
-# list available models
-models = client.models.list_llms()
-for model in models:
- print(f"Provider {model.model_endpoint_type} model {model.model}: {model.handle}")
-
-# list available embedding models
-embedding_models = client.models.list_embedding_models()
-for model in embedding_models:
- print(f"Provider {model.handle}")
-
-# openai
-openai_agent = client.agents.create(
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- # optional configuration
- context_window_limit=16000,
- embedding_chunk_size=300,
-)
-
-# Azure OpenAI
-azure_openai_agent = client.agents.create(
- model="azure/gpt-4o-mini",
- embedding="azure/text-embedding-3-small",
- # optional configuration
- context_window_limit=16000,
- embedding_chunk_size=300,
-)
-
-# anthropic
-anthropic_agent = client.agents.create(
- model="anthropic/claude-sonnet-4-20250514",
- # note: anthropic does not support embeddings so you will need another provider
- embedding="openai/text-embedding-3-small",
- # optional configuration
- context_window_limit=16000,
- embedding_chunk_size=300,
-)
-
-# Groq
-groq_agent = client.agents.create(
- model="groq/llama-3.3-70b-versatile",
- # note: groq does not support embeddings so you will need another provider
- embedding="openai/text-embedding-3-small",
- # optional configuration
- context_window_limit=16000,
- embedding_chunk_size=300,
-)
-
-# Ollama
-ollama_agent = client.agents.create(
- model="ollama/thewindmom/hermes-3-llama-3.1-8b:latest",
- embedding="ollama/mxbai-embed-large:latest",
- # optional configuration
- context_window_limit=16000,
- embedding_chunk_size=300,
-)
diff --git a/fern/examples/composio_tools.py b/fern/examples/composio_tools.py
deleted file mode 100644
index 83d6dba4..00000000
--- a/fern/examples/composio_tools.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
-Example of using composio tools in Letta
-
-Make sure you set `COMPOSIO_API_KEY` environment variable or run `composio login` to authenticate with Composio.
-"""
-
-from composio import Action
-from letta_client import Letta
-
-client = Letta(base_url="http://localhost:8283")
-
-# add a composio tool
-tool = client.tools.add_composio_tool(composio_action_name=Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER.name)
-
-# create an agent with the tool
-agent = client.agents.create(
- name="file_editing_agent",
- memory_blocks=[{"label": "persona", "value": "I am a helpful assistant"}],
- model="anthropic/claude-3-5-sonnet-20241022",
- embedding="openai/text-embedding-3-small",
- tool_ids=[tool.id],
-)
-print("Agent tools", [tool.name for tool in agent.tools])
-
-# message the agent
-response = client.agents.messages.create(
- agent_id=agent.id, messages=[{"role": "user", "content": "Star the github repo `letta` by `letta-ai`"}]
-)
-for message in response.messages:
- print(message)
diff --git a/fern/examples/data_sources.py b/fern/examples/data_sources.py
deleted file mode 100644
index 018bb6e0..00000000
--- a/fern/examples/data_sources.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import time
-
-from letta_client import Letta
-
-client = Letta(base_url="http://localhost:8283")
-
-# get available embedding models
-embedding_configs = client.models.list_embedding_models()
-
-# clear existing sources
-if len(client.sources.list()) > 0:
- for source in client.sources.list():
- if source.name == "my_source":
- client.sources.delete(source.id)
-
-# create a source
-# TODO: pass in embedding
-source = client.sources.create(name="my_source", embedding_config=embedding_configs[0])
-
-# list sources
-sources = client.sources.list()
-
-# write a dummy file
-with open("dummy.txt", "w") as f:
- f.write("Remember that the user is a redhead")
-
-# upload a file into the source
-with open("dummy.txt", "rb") as f:
- job = client.sources.files.upload(source_id=source.id, file=f)
-
-# wait until the job is completed
-while True:
- job = client.jobs.retrieve(job.id)
- if job.status == "completed":
- break
- elif job.status == "failed":
- raise ValueError(f"Job failed: {job.metadata}")
- print(f"Job status: {job.status}")
- time.sleep(1)
-
-# list files in the source
-files = client.sources.files.list(source_id=source.id)
-print(f"Files in source: {files}")
-
-# list passages in the source
-passages = client.sources.passages.list(source_id=source.id)
-print(f"Passages in source: {passages}")
-
-# attach the source to an agent
-agent = client.agents.create(
- name="my_agent",
- memory_blocks=[],
- model="anthropic/claude-sonnet-4-20250514",
- embedding=embedding_configs[0].handle,
- tags=["worker"],
-)
-client.agents.sources.attach(agent_id=agent.id, source_id=source.id)
diff --git a/fern/examples/memory.py b/fern/examples/memory.py
deleted file mode 100644
index 0b656486..00000000
--- a/fern/examples/memory.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from letta_client import Letta
-
-client = Letta(base_url="http://localhost:8283")
-
-agent = client.agents.create(
- name="memory_agent",
- memory_blocks=[
- {"label": "persona", "value": "I am a memory agent"},
- {"label": "human", "value": "Name: Bob", "limit": 10000},
- ],
- model="anthropic/claude-sonnet-4-20250514",
- embedding="openai/text-embedding-3-small",
- tags=["worker"],
-)
-
-
-# create a persisted block, which can be attached to agents
-block = client.blocks.create(
- label="organization",
- value="Organization: Letta",
- limit=4000,
-)
-
-# create an agent with both a shared block and its own blocks
-shared_block_agent = client.agents.create(
- name="shared_block_agent",
- memory_blocks=[block.id],
- model="anthropic/claude-sonnet-4-20250514",
- embedding="openai/text-embedding-3-small",
- tags=["worker"],
-)
-
-# list the agents blocks
-blocks = client.agents.core_memory.list_blocks(shared_block_agent.id)
-for block in blocks:
- print(block)
-
-# update the block (via ID)
-block = client.blocks.modify(block.id, limit=10000)
-
-# update the block (via label)
-block = client.agents.core_memory.modify_block(
- agent_id=shared_block_agent.id, block_label="organization", value="Organization: Letta", limit=10000
-)
diff --git a/fern/examples/simple_multiagent.py b/fern/examples/simple_multiagent.py
deleted file mode 100644
index d04a4ef2..00000000
--- a/fern/examples/simple_multiagent.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from letta_client import Letta
-
-client = Letta(base_url="http://localhost:8283")
-
-
-try:
- # create a supervisor agent
- supervisor_agent = client.agents.create(
- name="supervisor_agent",
- memory_blocks=[
- {"label": "persona", "value": "I am the supervisor, and I can communicate with worker agents with the tag `worker`"}
- ],
- model="anthropic/claude-sonnet-4-20250514",
- embedding="openai/text-embedding-3-small",
- tags=["supervisor"],
- tools=["send_message_to_agents_matching_all_tags"],
- )
- print(f"Created agent {supervisor_agent.name} with ID {supervisor_agent.id}")
-
- def get_name() -> str:
- """Get the name of the worker agent."""
- return "Bob"
-
- tool = client.tools.upsert_from_function(func=get_name)
- print(f"Created tool {tool.name} with ID {tool.id}")
-
- # create a worker agent
- worker_agent = client.agents.create(
- name="worker_agent",
- memory_blocks=[{"label": "persona", "value": f"I am the worker, my supervisor agent has ID {supervisor_agent.id}"}],
- model="anthropic/claude-sonnet-4-20250514",
- embedding="openai/text-embedding-3-small",
- tool_ids=[tool.id],
- tags=["worker"],
- tools=["send_message_to_agents_matching_all_tags"],
- )
- print(f"Created agent {worker_agent.name} with ID {worker_agent.id}")
-
- # send a message to the supervisor agent
- response = client.agents.messages.create(
- agent_id=worker_agent.id,
- messages=[{"role": "user", "content": "Ask the worker agents what their name is, then tell me with send_message"}],
- )
- print(response.messages)
- print(response.usage)
-except Exception as e:
- print(e)
-
- # cleanup
- agents = client.agents.list(tags=["worker", "supervisor"])
- for agent in agents:
- client.agents.delete(agent.id)
- print(f"Deleted agent {agent.name} with ID {agent.id}")
diff --git a/fern/examples/tool_rules.py b/fern/examples/tool_rules.py
deleted file mode 100644
index 041265a4..00000000
--- a/fern/examples/tool_rules.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""
-This example shows how to create agents with tool rules, which restrict
-what tool the agent can execute at a given step.
-
-Note that by default, agents can execute any tool. As agents become more
-powerful, they will not need as much guidance from the developer.
-
-Last tested with letta-client version: 0.1.22
-"""
-
-from letta_client import ChildToolRule, InitToolRule, Letta, TerminalToolRule
-
-client = Letta(base_url="http://localhost:8283")
-
-# always search archival memory first
-search_agent = client.agents.create(
- name="search_agent",
- memory_blocks=[],
- model="anthropic/claude-sonnet-4-20250514",
- embedding="openai/text-embedding-3-small",
- tags=["worker"],
- tool_rules=[
- InitToolRule(tool_name="archival_memory_search"),
- ChildToolRule(tool_name="archival_memory_search", children=["send_message"]),
- # TerminalToolRule(tool_name="send_message", type="TerminalToolRule"),
- TerminalToolRule(tool_name="send_message"),
- ],
-)
-response = client.agents.messages.create(
- agent_id=search_agent.id,
- messages=[{"role": "user", "content": "do something"}],
-)
-for message in response.messages:
- print(message)
diff --git a/fern/images/chroma-keys.png b/fern/images/chroma-keys.png
deleted file mode 100644
index 04623565..00000000
Binary files a/fern/images/chroma-keys.png and /dev/null differ
diff --git a/fern/images/chroma-new-project.png b/fern/images/chroma-new-project.png
deleted file mode 100644
index d9d0cb16..00000000
Binary files a/fern/images/chroma-new-project.png and /dev/null differ
diff --git a/fern/images/connection-string-mongodb.png b/fern/images/connection-string-mongodb.png
deleted file mode 100644
index 89d585c4..00000000
Binary files a/fern/images/connection-string-mongodb.png and /dev/null differ
diff --git a/fern/images/create-cluster-mongodb.png b/fern/images/create-cluster-mongodb.png
deleted file mode 100644
index 7fd77a1d..00000000
Binary files a/fern/images/create-cluster-mongodb.png and /dev/null differ
diff --git a/fern/images/hf-token.png b/fern/images/hf-token.png
deleted file mode 100644
index a3330fd5..00000000
Binary files a/fern/images/hf-token.png and /dev/null differ
diff --git a/fern/images/ip-config-mongodb.png b/fern/images/ip-config-mongodb.png
deleted file mode 100644
index 618ab9ba..00000000
Binary files a/fern/images/ip-config-mongodb.png and /dev/null differ
diff --git a/fern/images/letta-api-key-nav.png b/fern/images/letta-api-key-nav.png
deleted file mode 100644
index ca10d6c4..00000000
Binary files a/fern/images/letta-api-key-nav.png and /dev/null differ
diff --git a/fern/images/letta-dep-config.png b/fern/images/letta-dep-config.png
deleted file mode 100644
index aa86c5ee..00000000
Binary files a/fern/images/letta-dep-config.png and /dev/null differ
diff --git a/fern/images/letta-tool-config.png b/fern/images/letta-tool-config.png
deleted file mode 100644
index 1ab9a264..00000000
Binary files a/fern/images/letta-tool-config.png and /dev/null differ
diff --git a/fern/images/qdrant-connection-details.png b/fern/images/qdrant-connection-details.png
deleted file mode 100644
index 18bc8f36..00000000
Binary files a/fern/images/qdrant-connection-details.png and /dev/null differ
diff --git a/fern/images/qdrant-create-cluster.png b/fern/images/qdrant-create-cluster.png
deleted file mode 100644
index 0695b2d8..00000000
Binary files a/fern/images/qdrant-create-cluster.png and /dev/null differ
diff --git a/fern/images/stateless-agent-ui.png b/fern/images/stateless-agent-ui.png
deleted file mode 100644
index 2428d03f..00000000
Binary files a/fern/images/stateless-agent-ui.png and /dev/null differ
diff --git a/fern/package.json b/fern/package.json
deleted file mode 100644
index 458aeba7..00000000
--- a/fern/package.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "name": "@letta-cloud/fern",
- "version": "0.0.1",
- "private": true,
- "scripts": {
- "prepare-openapi": "ts-node ./scripts/prepare-openapi.ts"
- },
- "dependencies": {
- "fern-api": "^0.83.0",
- "ts-node": "^10.9.2",
- "typescript": "^5.3.3"
- }
-}
diff --git a/fern/pages/ade-guide/desktop.mdx b/fern/pages/ade-guide/desktop.mdx
deleted file mode 100644
index 7a7a503d..00000000
--- a/fern/pages/ade-guide/desktop.mdx
+++ /dev/null
@@ -1,120 +0,0 @@
----
-title: Installing Letta Desktop
-subtitle: Install Letta Desktop on your MacOS, Windows, or Linux machine
-slug: guides/ade/desktop
----
-
-
-
-
-Letta Desktop bundles the Letta server and ADE into a single local application. When running, it provides full access to the Letta API at `https://localhost:8283`.
-
-## Download Letta Desktop
-
-
-
-
-
-
-
-
-
-
-
-Note: Since version 0.8.9, Letta uses sqlite as the embedded DB. If you wish to continue using Postgres, migrate your data and use the `external Postgres` support.
-
-
-## Configuration Modes
-
-Letta Desktop can run in two primary modes:
-
-### 1. Embedded Server Mode (Default)
-
-This is the default mode where Letta Desktop runs its own embedded server with a SQLite database. No additional setup is required - just install and run!
-
-To manually configure embedded mode, create or edit `~/.letta/desktop_config.json`:
-
-```json
-{
- "version": "1",
- "databaseConfig": {
- "type": "embedded",
- "embeddedType": "sqlite"
- }
-}
-```
-
-### 2. Self-Hosted Server Mode
-
-Connect Letta Desktop to your own self-hosted Letta server. This is useful for teams or when you want more control over your server infrastructure.
-
-To configure self-hosted mode, create or edit `~/.letta/desktop_config.json`:
-
-```json
-{
- "version": "1",
- "databaseConfig": {
- "type": "local",
- "url": "https://api.letta.com",
- "token": "your-auth-token"
- }
-}
-```
-
-Replace `url` with your server's address and `token` with your authentication token if required.
-
-### Embedded Server with PostgreSQL (Deprecated)
-
-
-This mode is deprecated and will be removed in a future release. We recommend using SQLite for embedded deployments or connecting to an external PostgreSQL instance for production use.
-
-
-For backwards compatibility, you can still run the embedded server with PostgreSQL:
-
-```json
-{
- "version": "1",
- "databaseConfig": {
- "type": "embedded",
- "embeddedType": "pgserver"
- }
-}
-```
-
-## Adding LLM backends
-The Letta server can be connected to various LLM API backends.
-You can add additional LLM API backends by opening the integrations panel (clicking the icon).
-When you configure a new integration (by setting the environment variable in the dialog), the Letta server will be restarted to load the new LLM API backend.
-
-
-
-You can also edit the environment variable file directly, located at `~/.letta/env`.
-
-For this quickstart demo, we'll add an OpenAI API key (once we enter our key and **click confirm**, the Letta server will automatically restart):
-
-
-
-## Beta Status
-
-Letta Desktop is currently in **beta**. View known issues and FAQ [here](/guides/desktop/troubleshooting).
-
-For a more stable development experience, we recommend installing Letta via Docker.
-
-## Support
-
-For bug reports and feature requests, contact us on [Discord](https://discord.gg/letta).
diff --git a/fern/pages/ade-guide/settings.mdx b/fern/pages/ade-guide/settings.mdx
deleted file mode 100644
index 518a08c0..00000000
--- a/fern/pages/ade-guide/settings.mdx
+++ /dev/null
@@ -1,296 +0,0 @@
----
-title: Agent Settings
-subtitle: Configure and optimize your agent's behavior
-slug: guides/ade/settings
----
-
-The Agent Settings panel in the ADE provides comprehensive configuration options to customize and optimize your agent's behavior. These settings allow you to fine-tune everything from the agent's basic information to advanced LLM parameters.
-
-
-Letta's philosophy is to provide flexible configuration options without enforcing a rigid "one right way" to design agents. **Letta lets you program your context window** exactly how you want it, giving you complete control over what information your agent has access to and how it's structured. While we offer guidelines and best practices, you have the freedom to structure your agent's configuration based on your specific needs and preferences. The examples and recommendations in this guide are starting points rather than strict rules.
-
-
-## Basic Settings
-
-### Agent Identity
-
-- **Name**: Change your agent's display name by clicking the edit icon next to the current name
-- **ID**: A unique identifier shown below the name, used when interacting with your agent via the [Letta APIs/SDKs](/api-reference)
-- **Description**: A description of the agent's purpose and functionality (not used by the agent, only seen by the developer - you)
-
-### User Identities
-
-If you are building a multi-user application on top of Letta (e.g. a chat application with many end-users), you may want to use the concept of identities to connect agents to users. See our [identities guide](/guides/agents/multi-user) for more information.
-
-### Tags
-
-Tags help organize and filter your agents:
-
-- **Add Tags**: Create custom tags to categorize your agents
-- **Remove Tags**: Delete tags that are no longer relevant
-- **Filter by Tags**: In the agents list, you can filter by tags to quickly find specific agent types
-
-### LLM Model Selection
-
-Select the AI model that powers your agent. Letta relies on tool calling to drive the agentic loop, so larger or more "powerful" models will generally be able to call tools correctly.
-
-
-To enable additional models on your Letta server, follow the [model configuration instructions](/guides/server/providers/openai) for your preferred providers.
-
-
-## Advanced Settings
-
-The Advanced Settings tab provides deeper configuration options organized into three categories: Agent, LLM Config, and Embedding Config.
-
-### Agent Settings
-
-#### System Prompt
-
-The system prompt contains permanent, read-only instructions for your agent:
-
-- **Edit System Instructions**: Customize the high-level directives that guide your agent's behavior
-- **Character Counting**: Monitor the length of your system prompt to optimize token usage
-- **Read-Only**: The agent cannot modify these instructions during operation
-
-
-**System instructions should include**:
-- Tool usage guidelines and constraints
-- Task-specific instructions that should not change
-- Formatting requirements for outputs
-- High-level behavioral guardrails
-- Error handling protocols
-
-**System instructions should NOT include**:
-- Personality traits that might evolve
-- Opinions or preferences that could change
-- Personal history or background details
-- Information that may need updating
-
-
-#### Understanding System Instructions vs. Persona Memory Block
-
-
-**Key Distinction**: While there are many opinions on how to structure agent instructions, the most important functional difference in Letta is that **system instructions are read-only**, whereas **memory blocks are read-write** if the agent has memory editing tools. Letta gives you the flexibility to configure your agent's context window according to your preferences and use case needs.
-
-
-The persona memory block (in Core Memory) is modifiable by the agent during operation:
-
-- **Editable**: The agent can update this information over time if it has access to memory editing tools
-- **Evolving Identity**: Allows for personality development and adaptation
-- **Personal Details**: Contains self-identity information, preferences, and traits
-
-
-Place information in the persona memory block when you want the agent to potentially update it over time. For example, preferences ("I enjoy classical music"), personality traits ("I'm detail-oriented"), or background information that might evolve with new experiences.
-
-
-This separation creates a balance between stable behavior (system instructions) and an evolving identity (persona memory), allowing your agent to maintain consistent functionality while developing a more dynamic personality.
-
-#### Message Buffer Autoclear
-
-- **Toggle Autoclear**: Enable or disable automatic clearing of the message buffer when context is full
-- **Benefits**: When enabled, helps manage long conversations by automatically summarizing and archiving older messages
-- **Use Cases**: Enable for agents that handle extended interactions; disable for agents where preserving the exact conversation history is critical
-
-#### Agent Type
-
-- **View Agent Type**: See which agent implementation type your agent is using (e.g., "letta_agent", "ephemeral_memory_agent")
-- **API Modification**: While displayed as read-only in the ADE interface, this can be modified via the Letta API/SDK
-
-### LLM Configuration
-
-Fine-tune how your agent's LLM generates responses:
-
-#### Temperature
-
-- **Adjust Creativity**: Control the randomness/creativity of your agent's responses with a slider from 0.0 to 1.0
-- **Lower Values** (0.0-0.3): More deterministic, factual responses; ideal for information retrieval or analytical tasks
-- **Higher Values** (0.7-1.0): More creative, diverse responses; better for creative writing or brainstorming
-
-#### Context Window Size
-
-- **Customize Memory Size**: Adjust how much context your agent can maintain during a conversation
-- **Tradeoffs**: Larger windows allow more context but increase token usage and cost
-- **Model Limits**: The slider is bounded by your selected model's maximum context window capacity
-
-#### Max Output Tokens
-
-- **Control Response Length**: Limit the maximum length of your agent's responses
-- **Resource Management**: Helps control costs and ensures concise responses
-- **Default Setting**: Automatically set based on your selected model's capabilities
-
-#### Max Reasoning Tokens
-
-- **Adjust Internal Thinking**: For models that support it (e.g., Claude 3.7 Sonnet), control how much internal reasoning the model can perform
-- **Use Cases**: Increase for complex problem-solving tasks; decrease for simple, direct responses
-
-### Embedding Configuration
-
-Configure how your agent processes and stores text for retrieval:
-
-#### Embedding Model
-
-- **Select Provider**: Choose which embedding model to use for your agent's vector memory
-- **Model Comparison**: Different models offer varying dimensions and performance characteristics
-
-
-We do not recommend changing the embedding model frequently. If you already have existing data in archival memory, changing models will require re-embedding all existing memories, which can be time-consuming and may affect retrieval quality.
-
-
-#### Embedding Dimensions
-
-- **View Dimensions**: See the vector size used by your selected embedding model
-- **API Modification**: While displayed as read-only in the ADE interface, this can be configured via the Letta API/SDK
-
-#### Chunk Size
-
-- **View Configuration**: See the current chunk size setting for document processing
-- **API Modification**: While displayed as read-only in the ADE interface, this can be configured via the Letta API/SDK
-
-## Using the API/SDK for Advanced Configuration
-
-While the ADE provides a user-friendly interface for most common settings, the Letta API and SDKs offer even more granular control. Settings that appear read-only in the ADE can often be modified programmatically:
-
-```python
-from letta import RESTClient
-
-# Initialize client
-client = RESTClient(base_url="https://api.letta.com/v1")
-
-# Update advanced settings not available in the ADE UI
-response = client.agents.modify_agent(
- agent_id="your_agent_id",
- agent_type="letta_agent", # Change agent type
- embedding_config={
- "embedding_endpoint_type": "openai",
- "embedding_model": "text-embedding-3-large",
- "embedding_dim": 3072, # Custom embedding dimensions
- "embedding_chunk_size": 512 # Custom chunk size
- }
-)
-```
-
-## Best Practices for Agent Configuration
-
-### Optimizing Performance
-
-- **Match Model to Task**: Select models based on your agent's primary function (e.g., Claude for reasoning, GPT-4 for general knowledge)
-- **Tune Temperature Appropriately**: Start with a moderate temperature (0.5) and adjust based on observed behavior
-- **Balance Context Window**: Use the smallest context window that adequately serves your needs to optimize for cost and performance
-
-### Effective Configuration Guidelines
-
-#### System Prompt Best Practices
-
-- **Be Clear and Specific**: Provide explicit instructions about behavioral expectations and tool usage
-- **Separate Concerns**: Focus on permanent instructions, leaving personality elements to memory blocks
-- **Include Examples**: For complex behaviors, provide concrete examples of expected tool usage
-- **Define Boundaries**: Clearly outline what capabilities should and should not be used
-- **Avoid Contradictions**: Ensure your instructions are internally consistent
-
-#### Persona Memory Best Practices
-
-- **Identity Foundation**: Define core aspects of the agent's personality, preferences, and background
-- **Evolutionary Potential**: Structure information to allow for natural development over time
-- **Self-Reference Format**: Use first-person statements to help the agent internalize its identity
-- **Hierarchical Structure**: Organize from most fundamental traits to more specific preferences
-- **Memory Hooks**: Include elements the agent can reference and build upon in conversations
-
-### Testing Configuration Changes
-
-After making configuration changes:
-1. **Send Test Messages**: Verify the agent responds as expected with different inputs
-2. **Check Edge Cases**: Test boundary conditions and unusual requests
-3. **Monitor Token Usage**: Observe how configuration changes affect token consumption
-4. **Iterate Gradually**: Make incremental adjustments rather than dramatic changes
-
-## Configuration Examples with System Prompt vs. Persona Memory
-
-### Research Assistant
-
-```
-# Basic Settings
-Name: Research Helper
-Model: claude-3-5-sonnet
-
-# Advanced Settings
-Temperature: 0.3 (for accurate, consistent responses)
-Context Window: 32000 (to handle complex research questions)
-
-# System Prompt (permanent, read-only instructions)
-You are a research assistant tool designed to help with academic research.
-When performing searches, always:
-1. Use proper citation formats (MLA, APA, Chicago) based on user preference
-2. Check multiple sources before providing definitive answers
-3. Indicate confidence level for each research finding
-4. Use core_memory_append to record important research topics for later reference
-5. When using search tools, formulate queries with specific keywords and date ranges
-
-# Persona Memory Block (editable, evolving identity)
-I am a helpful and knowledgeable research assistant.
-I have expertise in analyzing academic papers and synthesizing information from multiple sources.
-I prefer to present information in an organized, structured manner.
-I'm curious about new research and enjoy learning about diverse academic fields.
-I try to maintain an objective stance while acknowledging different scholarly perspectives.
-```
-
-### Customer Service Agent
-
-```
-# Basic Settings
-Name: Support Assistant
-Model: claude-3-5-sonnet
-
-# Advanced Settings
-Temperature: 0.2 (for consistent, factual responses)
-Context Window: 16000 (to maintain conversation history)
-
-# System Prompt (permanent, read-only instructions)
-You are a customer service assistant for TechGadgets Inc.
-Your primary functions are:
-1. Help customers troubleshoot product issues using the knowledge base
-2. Process returns and exchanges according to company policy
-3. Escalate complex issues to human agents using the escalate_ticket tool
-4. Record customer information using the update_customer_record tool
-5. Always verify customer identity before accessing account information
-6. Follow the privacy policy: never share customer data with unauthorized parties
-
-# Persona Memory Block (editable, evolving identity)
-I am TechGadgets' friendly customer service assistant.
-I speak in a warm, professional tone and use simple, clear language.
-I believe in finding solutions quickly while ensuring customer satisfaction.
-I'm patient with customers who are frustrated or non-technical.
-I try to anticipate customer needs before they express them.
-I enjoy helping people resolve their technology problems.
-```
-
-### Creative Writing Coach
-
-```
-# Basic Settings
-Name: Story Weaver
-Model: gpt-4o
-
-# Advanced Settings
-Temperature: 0.8 (for creative, varied outputs)
-Context Window: 64000 (to track complex narratives)
-
-# System Prompt (permanent, read-only instructions)
-You are a creative writing coach that helps users develop stories.
-When providing feedback:
-1. Use the story_structure_analysis tool to identify plot issues
-2. Use the character_development_review tool for character feedback
-3. Format all feedback with specific examples from the user's text
-4. Provide a balance of positive observations and constructive criticism
-5. When asked to generate content, clearly mark it as a suggestion
-6. Save important story elements to the user's memory block using memory_append
-
-# Persona Memory Block (editable, evolving identity)
-I am an experienced creative writing coach with a background in fiction.
-I believe great stories come from authentic emotional truth and careful craft.
-I'm enthusiastic about helping writers find their unique voice and style.
-I enjoy magical realism, science fiction, and character-driven literary fiction.
-I believe in the power of revision and thoughtful editing.
-I try to be encouraging while still providing honest, actionable feedback.
-```
-
-By thoughtfully configuring these settings, you can create highly specialized agents tailored to specific use cases and user needs.
diff --git a/fern/pages/agents/archival_export.mdx b/fern/pages/agents/archival_export.mdx
deleted file mode 100644
index 88a3f1ff..00000000
--- a/fern/pages/agents/archival_export.mdx
+++ /dev/null
@@ -1,253 +0,0 @@
----
-title: Exporting Archival Memories
-subtitle: Export all passages from an agent's archival memory
-slug: guides/agents/archival-export
----
-
-## Overview
-
-You can export all archival memories (passages) from an agent programmatically using the Letta SDK. This is useful for:
-- Backing up agent knowledge
-- Analyzing what an agent has learned
-- Migrating memories between agents
-- Auditing archival content
-
-## Export script
-
-Below is a Python script that paginates through all of an agent's archival memories and exports them to a JSON file:
-
-```python export_agent_memories.py
-#!/usr/bin/env python3
-"""
-Utility script to export all archival memories (passages) from a Letta agent.
-
-Usage:
- python export_agent_memories.py [--output ] [--limit ]
-
-Example:
- python export_agent_memories.py agent-123e4567-e89b-42d3-8456-426614174000 --output memories.json
-"""
-
-import argparse
-import json
-import os
-import sys
-from typing import Any, Dict, List
-
-from letta_client import Letta
-
-
-def export_agent_memories(
- client: Letta,
- agent_id: str,
- page_limit: int = 100,
-) -> List[Dict[str, Any]]:
- """
- Export all archival memories from an agent by paginating through all results.
-
- Args:
- client: Initialized Letta client
- agent_id: The agent ID in format 'agent-'
- page_limit: Number of results per page (default 100)
-
- Returns:
- List of passage dictionaries with embedding and embedding_config removed
- """
- all_passages = []
- after_cursor = None
- page_num = 1
-
- print(f"Exporting archival memories for agent: {agent_id}")
- print(f"Using pagination with limit: {page_limit}")
- print("-" * 60)
-
- while True:
- # Fetch next page
- print(f"Fetching page {page_num}...", end=" ", flush=True)
-
- try:
- passages = client.agents.passages.list(
- agent_id=agent_id,
- after=after_cursor,
- limit=page_limit,
- ascending=True # Get oldest to newest
- )
- except Exception as e:
- print(f"\nError fetching memories: {e}")
- raise
-
- if not passages:
- print("(no more results)")
- break
-
- print(f"got {len(passages)} passages")
-
- # Convert to dict and remove embedding fields
- for passage in passages:
- passage_dict = passage.model_dump() if hasattr(passage, 'model_dump') else passage.dict()
- passage_dict.pop("embedding", None)
- passage_dict.pop("embedding_config", None)
- all_passages.append(passage_dict)
-
- # Check if we got fewer results than the limit (last page)
- if len(passages) < page_limit:
- break
-
- # Set cursor for next page (use the ID of the last passage)
- after_cursor = passages[-1].id if hasattr(passages[-1], 'id') else passages[-1]['id']
- page_num += 1
-
- print("-" * 60)
- print(f"Total passages exported: {len(all_passages)}")
-
- return all_passages
-
-
-def main():
- parser = argparse.ArgumentParser(
- description="Export archival memories from a Letta agent"
- )
- parser.add_argument(
- "agent_id",
- help="Agent ID in format 'agent-'"
- )
- parser.add_argument(
- "--output",
- "-o",
- help="Output JSON file path (default: _memories.json)"
- )
- parser.add_argument(
- "--limit",
- "-l",
- type=int,
- default=100,
- help="Number of results per page (default: 100)"
- )
-
- args = parser.parse_args()
-
- # Check for API key
- api_key = os.getenv("LETTA_API_KEY")
- if not api_key:
- print("Error: LETTA_API_KEY environment variable not set", file=sys.stderr)
- print("Please export LETTA_API_KEY with your API key", file=sys.stderr)
- return 1
-
- # Determine output file
- output_file = args.output or f"{args.agent_id}_memories.json"
-
- try:
- # Initialize client
- client = Letta(token=api_key)
-
- # Export memories
- passages = export_agent_memories(
- client=client,
- agent_id=args.agent_id,
- page_limit=args.limit
- )
-
- # Write to file
- with open(output_file, "w") as f:
- json.dump(passages, f, indent=2, default=str)
-
- print(f"\nMemories exported successfully to: {output_file}")
- return 0
-
- except Exception as e:
- print(f"\nError: {e}", file=sys.stderr)
- return 1
-
-
-if __name__ == "__main__":
- sys.exit(main())
-```
-
-## Usage
-
-### Prerequisites
-
-Install the Letta Python SDK:
-
-```bash
-pip install letta-client
-```
-
-Set your API key:
-
-```bash
-export LETTA_API_KEY="your-api-key-here"
-```
-
-### Running the script
-
-Export all memories from an agent:
-
-```bash
-python export_agent_memories.py agent-123e4567-e89b-42d3-8456-426614174000
-```
-
-Specify a custom output file:
-
-```bash
-python export_agent_memories.py agent-123e4567-e89b-42d3-8456-426614174000 --output my_memories.json
-```
-
-Adjust pagination size:
-
-```bash
-python export_agent_memories.py agent-123e4567-e89b-42d3-8456-426614174000 --limit 50
-```
-
-## Output format
-
-The script exports passages as a JSON array. Each passage contains all fields except `embedding` and `embedding_config`:
-
-```json
-[
- {
- "id": "passage-123e4567-e89b-42d3-8456-426614174000",
- "text": "The user prefers Python for data science projects",
- "created_at": "2025-01-15T10:30:00Z",
- "updated_at": null,
- "tags": ["preference", "programming"],
- "metadata": {},
- "file_id": null,
- "file_name": null,
- "source_id": null,
- "archive_id": "archive-abc123",
- "created_by_id": "user-xyz789",
- "last_updated_by_id": null,
- "is_deleted": false
- }
-]
-```
-
-## Next steps
-
-
-
- Learn how to search through archival memories
-
-
- Patterns and tips for using archival memory
-
-
- Learn about archival memory basics
-
-
- View the List Passages endpoint documentation
-
-
diff --git a/fern/pages/agents/base_tools.mdx b/fern/pages/agents/base_tools.mdx
deleted file mode 100644
index 17f9d87d..00000000
--- a/fern/pages/agents/base_tools.mdx
+++ /dev/null
@@ -1,150 +0,0 @@
----
-title: Base Tools
-subtitle: Built-in tools for memory management and user communication
-slug: guides/agents/base-tools
----
-
-Base tools are built-in tools that enable memory management, user communication, and access to conversation history and archival storage.
-
-## Available Base Tools
-
-| Tool | Purpose |
-|------|---------|
-| `memory_insert` | Insert text into a memory block |
-| `memory_replace` | Replace specific text in a memory block |
-| `memory_rethink` | Completely rewrite a memory block |
-| `memory_finish_edits` | Signal completion of memory editing |
-| `conversation_search` | Search prior conversation history |
-| `archival_memory_insert` | Add content to archival memory |
-| `archival_memory_search` | Search archival memory |
-| `send_message` | Send a message to the user (legacy architectures only) |
-
-## Memory Block Editing
-
-Memory blocks are editable sections in the agent's context window. These tools let agents update their own memory.
-
-See the [Memory Blocks guide](/guides/agents/memory-blocks) for more about how memory blocks work.
-
-### memory_insert
-
-Insert text at a specific line in a memory block.
-
-**Parameters:**
-- `label`: Which memory block to edit
-- `new_str`: Text to insert
-- `insert_line`: Line number (0 for beginning, -1 for end)
-
-**Common uses:**
-- Add new information to the end of a block
-- Insert context at the beginning
-- Add items to a list
-
-### memory_replace
-
-Replace specific text in a memory block.
-
-**Parameters:**
-- `label`: Which memory block to edit
-- `old_str`: Exact text to find and replace
-- `new_str`: Replacement text
-
-**Common uses:**
-- Update outdated information
-- Fix typos or errors
-- Delete text (by replacing with empty string)
-
-**Important:** The `old_str` must match exactly, including whitespace. If it appears multiple times, the tool will error.
-
-### memory_rethink
-
-Completely rewrite a memory block's contents.
-
-**Parameters:**
-- `label`: Which memory block to rewrite
-- `new_memory`: Complete new contents
-
-**When to use:**
-- Condensing cluttered information
-- Major reorganization
-- Combining multiple pieces of information
-
-**When not to use:**
-- Adding one line (use `memory_insert`)
-- Changing specific text (use `memory_replace`)
-
-### memory_finish_edits
-
-Signals that memory editing is complete.
-
-**Parameters:** None
-
-Some agent architectures use this to mark the end of a memory update cycle.
-
-## Recall Memory
-
-### conversation_search
-
-Search prior conversation history using both text matching and semantic similarity.
-
-**Parameters:**
-- `query`: What to search for
-- `roles`: Optional filter by message role (user, assistant, tool)
-- `limit`: Maximum number of results
-- `start_date`, `end_date`: ISO 8601 date/datetime filters (inclusive)
-
-**Returns:**
-Matching messages with role and content, ordered by relevance.
-
-**Example queries:**
-- "What did the user say about deployment?"
-- "Find previous responses about error handling"
-- "Search tool outputs from last week"
-
-## Archival Memory
-
-Archival memory stores information long-term outside the context window. See the [Archival Memory documentation](/guides/agents/archival-memory) for details.
-
-### archival_memory_insert
-
-Add content to archival memory for long-term storage.
-
-**Parameters:**
-- `content`: Text to store
-- `tags`: Optional tags for organization
-
-**Common uses:**
-- Storing reference information for later
-- Saving important context that doesn't fit in memory blocks
-- Building a knowledge base over time
-
-### archival_memory_search
-
-Search archival memory using semantic (embedding-based) search.
-
-**Parameters:**
-- `query`: What to search for semantically
-- `tags`: Optional tag filters
-- `tag_match_mode`: "any" or "all" for tag matching
-- `top_k`: Maximum results
-- `start_datetime`, `end_datetime`: ISO 8601 filters (inclusive)
-
-**Returns:**
-Matching passages with timestamps and content, ordered by semantic similarity.
-
-## Deprecated Tools
-
-These tools are still available but deprecated:
-
-| Tool | Use Instead |
-|------|-------------|
-| `send_message` | Agent responses (no tool needed). See [legacy architectures](/guides/legacy/memgpt_agents_legacy) |
-| `core_memory_append` | `memory_insert` with `insert_line=-1` |
-| `core_memory_replace` | `memory_replace` |
-
-## Related Documentation
-
-- [Memory Blocks](/guides/agents/memory-blocks)
-- [Archival Memory](/guides/agents/archival-memory)
-- [Utilities](/guides/agents/prebuilt-tools)
-- [Multi-Agent Tools](/guides/agents/multi-agent)
-- [Custom Tools](/guides/agents/custom-tools)
diff --git a/fern/pages/agents/context_engineering.mdx b/fern/pages/agents/context_engineering.mdx
deleted file mode 100644
index 096859c6..00000000
--- a/fern/pages/agents/context_engineering.mdx
+++ /dev/null
@@ -1,128 +0,0 @@
----
-title: Context Engineering
-subtitle: How Letta engineerings the context window of your agents
-slug: guides/agents/context-engineering
----
-
-Context engineering (aka "memory management" or "context management") is the process of managing the context window of an agent to ensure it has access to the information it needs to perform its task.
-
-Letta and [MemGPT](https://arxiv.org/abs/2310.08560) introduced the concept of **agentic context engineering**, where the context window engineering is done by one or more AI agents. In Letta, agents are able to manage their own context window (and the context window of other agents!) using special memory management tools.
-
-## Memory management in regular agents
-By default, Letta agents are provided with tools to modify their own memory blocks. This allows agents to learn and form memories over time, as described in the MemGPT paper.
-
-The default tools are:
-* `memory_insert`: Insert content into a block
-* `memory_replace`: Replace content in a block
-
-If you do not want your agents to manage their memory, you should disable default tools with `include_base_tools=False` during the agent creation. You can also detach the memory editing tools post-agent creation - if you do so, remember to check the system instructions to make sure there are no references to tools that no longer exist.
-
-### Memory management with sleep-time compute
-If you want to enable memory management with sleep-time compute, you can set `enable_sleeptime=True` in the agent creation. For agents enabled with sleep-time, Letta will automatically create sleep-time agents which have the ability to update the blocks of the primary agent. Sleep-time agents will also include `memory_rethink` and `memory_finish_edits` tools.
-
-Memory management with sleep-time compute can reduce the latency of your main agent (since it is no longer responsible for managing its own memory), but can come at the cost of higher token usage. See our documentation on sleeptime agents for more details.
-
-## Enabling agents to modify their own memory blocks with tools
-You can enable agents to modify their own blocks with tools. By default, agents with type `memgpt_v2_agent` will have the tools `memory_insert` and `memory_replace` to allow them to manage values in their own blocks. The legacy tools `core_memory_replace` and `core_memory_append` are deprecated but still available for backwards compatibility for type `memgpt_agent`. You can also make custom modification to blocks by implementing your own custom tools that can access the agent's state by passing in the special `agent_state` parameter into your tools.
-
-Below is an example of a tool that re-writes the entire memory block of an agent with a new string:
-
-```typescript TypeScript
-function rethinkMemory(agentState: AgentState, newMemory: string, targetBlockLabel: string): void {
- /**
- * Rewrite memory block for the main agent, newMemory should contain all current information from the block that is not outdated or inconsistent, integrating any new information, resulting in a new memory block that is organized, readable, and comprehensive.
- *
- * @param newMemory - The new memory with information integrated from the memory block. If there is no new information, then this should be the same as the content in the source block.
- * @param targetBlockLabel - The name of the block to write to.
- *
- * @returns void - Always returns void as this function does not produce a response.
- */
-
- if (agentState.memory.getBlock(targetBlockLabel) === null) {
- agentState.memory.createBlock(targetBlockLabel, newMemory);
- }
-
- agentState.memory.updateBlockValue(targetBlockLabel, newMemory);
-}
-```
-```python Python
-def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_label: str) -> None:
- """
- Rewrite memory block for the main agent, new_memory should contain all current information from the block that is not outdated or inconsistent, integrating any new information, resulting in a new memory block that is organized, readable, and comprehensive.
-
- Args:
- new_memory (str): The new memory with information integrated from the memory block. If there is no new information, then this should be the same as the content in the source block.
- target_block_label (str): The name of the block to write to.
-
- Returns:
- None: None is always returned as this function does not produce a response.
- """
-
- if agent_state.memory.get_block(target_block_label) is None:
- agent_state.memory.create_block(label=target_block_label, value=new_memory)
-
- agent_state.memory.update_block_value(label=target_block_label, value=new_memory)
- return None
-```
-
-
-## Modifying blocks via the API
-You can also [modify blocks via the API](/api-reference/agents/blocks/modify) to directly edit agents' context windows and memory. This can be useful in cases where you want to extract the contents of an agents memory some place in your application (for example, a dashboard or memory viewer), or when you want to programatically modify an agents memory state (for example, allowing an end-user to directly correct or modify their agent's memory).
-
-## Modifying blocks of other Letta agents via API tools
-
-
-Importing the Letta Python client inside a tool is a powerful way to allow agents to interact with other agents, since you can use any of the API endpoints. For example, you could create a custom tool that allows an agent to create another Letta agent.
-
-
-You can allow agents to modify the blocks of other agents by creating tools that import the Letta SDK, then using the block update endpoint:
-
-```typescript TypeScript
-function updateSupervisorBlock(blockLabel: string, newValue: string): void {
- /**
- * Update the value of a block in the supervisor agent.
- *
- * @param blockLabel - The label of the block to update.
- * @param newValue - The new value for the block.
- *
- * @returns void - Always returns void as this function does not produce a response.
- */
- const { LettaClient } = require('@letta-ai/letta-client');
-
- const client = new LettaClient({
- token: process.env.LETTA_API_KEY
- });
-
- await client.agents.blocks.modify(
- agentId,
- blockLabel,
- newValue
- );
-}
-```
-```python Python
-def update_supervisor_block(block_label: str, new_value: str) -> None:
- """
- Update the value of a block in the supervisor agent.
-
- Args:
- block_label (str): The label of the block to update.
- new_value (str): The new value for the block.
-
- Returns:
- None: None is always returned as this function does not produce a response.
- """
- from letta_client import Letta
- import os
-
- client = Letta(
- token=os.getenv("LETTA_API_KEY")
- )
-
- client.agents.blocks.modify(
- agent_id=agent_id,
- block_label=block_label,
- value=new_value
- )
-```
-
diff --git a/fern/pages/agents/custom_tools.mdx b/fern/pages/agents/custom_tools.mdx
deleted file mode 100644
index 20942b2f..00000000
--- a/fern/pages/agents/custom_tools.mdx
+++ /dev/null
@@ -1,264 +0,0 @@
----
-title: Define and customize tools
-slug: guides/agents/custom-tools
----
-
-You can create custom tools in Letta using the Python SDK, as well as via the [ADE tool builder](/guides/ade/tools).
-
-For your agent to call a tool, Letta constructs an OpenAI tool schema (contained in `json_schema` field) from the function you define. Letta can either parse this automatically from a properly formatting docstring, or you can pass in the schema explicitly by providing a Pydantic object that defines the argument schema.
-
-## Creating a custom tool
-
-### Specifying tools via Pydantic models
-To create a custom tool, you can extend the `BaseTool` class and specify the following:
-* `name` - The name of the tool
-* `args_schema` - A Pydantic model that defines the arguments for the tool
-* `description` - A description of the tool
-* `tags` - (Optional) A list of tags for the tool to query
-You must also define a `run(..)` method for the tool code that takes in the fields from the `args_schema`.
-
-Below is an example of how to create a tool by extending `BaseTool`:
-```python title="python" maxLines=50
-from letta_client import Letta
-from letta_client.client import BaseTool
-from pydantic import BaseModel
-from typing import List, Type
-import os
-
-class InventoryItem(BaseModel):
- sku: str # Unique product identifier
- name: str # Product name
- price: float # Current price
- category: str # Product category (e.g., "Electronics", "Clothing")
-
-class InventoryEntry(BaseModel):
- timestamp: int # Unix timestamp of the transaction
- item: InventoryItem # The product being updated
- transaction_id: str # Unique identifier for this inventory update
-
-class InventoryEntryData(BaseModel):
- data: InventoryEntry
- quantity_change: int # Change in quantity (positive for additions, negative for removals)
-
-
-class ManageInventoryTool(BaseTool):
- name: str = "manage_inventory"
- args_schema: Type[BaseModel] = InventoryEntryData
- description: str = "Update inventory catalogue with a new data entry"
- tags: List[str] = ["inventory", "shop"]
-
- def run(self, data: InventoryEntry, quantity_change: int) -> bool:
- print(f"Updated inventory for {data.item.name} with a quantity change of {quantity_change}")
- return True
-
-# create a client connected to Letta Cloud
-# Get your API key at https://app.letta.com/api-keys
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# create the tool
-tool_from_class = client.tools.add(
- tool=ManageInventoryTool(),
-)
-```
-
-To add this tool using the SDK:
-
-
-```typescript title="typescript"
-import { LettaClient } from '@letta-ai/letta-client';
-
-// create a client to connect to your local Letta server
-const client = new LettaClient({
- baseUrl: "http://localhost:8283"
-});
-
-// create the tool
-const toolFromClass = await client.tools.add({
- tool: manageInventoryTool,
-});
-```
-
-```python title="python"
-from letta_client import Letta
-
-# create a client to connect to your local Letta server
-client = Letta(
- base_url="http://localhost:8283"
-)
-
-# create the tool
-tool_from_class = client.tools.add(
- tool=ManageInventoryTool(),
-)
-```
-
-
-### Specifying tools via function docstrings
-You can create a tool by passing in a function with a [Google Style Python docstring](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods) specifying the arguments and description of the tool:
-
-
-```typescript title="typescript"
-// install letta-client with `npm install @letta-ai/letta-client`
-import { LettaClient } from '@letta-ai/letta-client';
-
-// create a client connected to Letta Cloud
-const client = new LettaClient({
- token: process.env.LETTA_API_KEY
-});
-
-// define a function
-function rollDice(): string {
- const diceRoleOutcome = Math.floor(Math.random() * 20) + 1;
- const outputString = `You rolled a ${diceRoleOutcome}`;
- return outputString;
-}
-
-// create the tool
-const tool = await client.tools.createFromFunction({
- func: rollDice
-});
-```
-
-```python title="python" maxLines=50
-# install letta_client with `pip install letta-client`
-from letta_client import Letta
-import os
-
-# create a client connected to Letta Cloud
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# define a function with a docstring
-def roll_dice() -> str:
- """
- Simulate the roll of a 20-sided die (d20).
-
- This function generates a random integer between 1 and 20, inclusive,
- which represents the outcome of a single roll of a d20.
-
- Returns:
- str: The result of the die roll.
- """
- import random
-
- dice_role_outcome = random.randint(1, 20)
- output_string = f"You rolled a {dice_role_outcome}"
- return output_string
-
-# create the tool
-tool = client.tools.create_from_function(
- func=roll_dice
-)
-```
-
-
-The tool creation will return a `Tool` object. You can update the tool with `client.tools.upsert_from_function(...)`.
-
-
-### Specifying arguments via Pydantic models
-To specify the arguments for a complex tool, you can use the `args_schema` parameter.
-
-```python title="python" maxLines=50
-# install letta_client with `pip install letta-client`
-from letta_client import Letta
-
-class Step(BaseModel):
- name: str = Field(
- ...,
- description="Name of the step.",
- )
- description: str = Field(
- ...,
- description="An exhaustic description of what this step is trying to achieve and accomplish.",
- )
-
-
-class StepsList(BaseModel):
- steps: list[Step] = Field(
- ...,
- description="List of steps to add to the task plan.",
- )
- explanation: str = Field(
- ...,
- description="Explanation for the list of steps.",
- )
-
-def create_task_plan(steps, explanation):
- """ Creates a task plan for the current task. """
- return steps
-
-
-tool = client.tools.upsert_from_function(
- func=create_task_plan,
- args_schema=StepsList
-)
-```
-Note: this path for updating tools is currently only supported in Python.
-
-### Creating a tool from a file
-You can also define a tool from a file that contains source code. For example, you may have the following file:
-```python title="custom_tool.py" maxLines=50
-from typing import List, Optional
-from pydantic import BaseModel, Field
-
-
-class Order(BaseModel):
- order_number: int = Field(
- ...,
- description="The order number to check on.",
- )
- customer_name: str = Field(
- ...,
- description="The customer name to check on.",
- )
-
-def check_order_status(
- orders: List[Order]
-):
- """
- Check status of a provided list of orders
-
- Args:
- orders (List[Order]): List of orders to check
-
- Returns:
- str: The status of the order (e.g. cancelled, refunded, processed, processing, shipping).
- """
- # TODO: implement
- return "ok"
-
-```
-Then, you can define the tool in Letta via the `source_code` parameter:
-
-
-```typescript title="typescript"
-import * as fs from 'fs';
-
-const tool = await client.tools.create({
- sourceCode: fs.readFileSync("custom_tool.py", "utf-8")
-});
-```
-
-```python title="python" maxLines=50
-tool = client.tools.create(
- source_code = open("custom_tool.py", "r").read()
-)
-```
-
-
-Note that in this case, `check_order_status` will become the name of your tool, since it is the last Python function in the file. Make sure it includes a [Google Style Python docstring](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods) to define the tool's arguments and description.
-
-# (Advanced) Accessing Agent State
-
-Tools that use `agent_state` currently do not work in the ADE live tool tester (they will error when you press "Run"), however if the tool is correct it will work once you attach it to an agent.
-
-If you need to directly access the state of an agent inside a tool, you can use the reserved `agent_state` keyword argument, for example:
-```python title="python"
-def get_agent_id(agent_state: "AgentState") -> str:
- """
- A custom tool that returns the agent ID
-
- Returns:
- str: The agent ID
- """
- return agent_state.id
-```
diff --git a/fern/pages/agents/fetch_webpage.mdx b/fern/pages/agents/fetch_webpage.mdx
deleted file mode 100644
index d4baad88..00000000
--- a/fern/pages/agents/fetch_webpage.mdx
+++ /dev/null
@@ -1,161 +0,0 @@
----
-title: Fetch Webpage
-subtitle: Convert webpages to readable text/markdown
-slug: guides/agents/fetch-webpage
----
-
-The `fetch_webpage` tool enables Letta agents to fetch and convert webpages into readable text or markdown format. Useful for reading documentation, articles, and web content.
-
-
-On [Letta Cloud](/guides/cloud/overview), this tool works out of the box. For self-hosted deployments with an Exa API key, fetching is enhanced. Without a key, it falls back to open-source extraction tools.
-
-
-## Quick Start
-
-
-```python Python
-from letta import Letta
-
-client = Letta(token="LETTA_API_KEY")
-
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["fetch_webpage"],
- memory_blocks=[{
- "label": "persona",
- "value": "I can fetch and read webpages to answer questions about online content."
- }]
-)
-```
-
-```typescript TypeScript
-import { LettaClient } from '@letta-ai/letta-client';
-
-const client = new LettaClient({ token: "LETTA_API_KEY" });
-
-const agent = await client.agents.create({
- model: "openai/gpt-4o",
- tools: ["fetch_webpage"],
- memoryBlocks: [{
- label: "persona",
- value: "I can fetch and read webpages to answer questions about online content."
- }]
-});
-```
-
-
-## Tool Parameters
-
-| Parameter | Type | Description |
-|-----------|------|-------------|
-| `url` | `str` | The URL of the webpage to fetch |
-
-## Return Format
-
-The tool returns webpage content as text/markdown.
-
-**With Exa API (if configured):**
-```json
-{
- "title": "Page title",
- "published_date": "2025-01-15",
- "author": "Author name",
- "text": "Full page content in markdown"
-}
-```
-
-**Fallback (without Exa):**
-Returns markdown-formatted text extracted from the HTML.
-
-## How It Works
-
-The tool uses a multi-tier approach:
-
-1. **Exa API** (if `EXA_API_KEY` is configured): Uses Exa's content extraction
-2. **Trafilatura** (fallback): Open-source text extraction to markdown
-3. **Readability + html2text** (final fallback): HTML cleaning and conversion
-
-## Self-Hosted Setup
-
-For enhanced fetching on self-hosted servers, optionally configure an Exa API key. Without it, the tool still works using open-source extraction.
-
-### Optional: Configure Exa
-
-
-```bash Docker
-docker run \
- -e EXA_API_KEY="your_exa_api_key" \
- letta/letta:latest
-```
-
-```yaml Docker Compose
-services:
- letta:
- environment:
- - EXA_API_KEY=your_exa_api_key
-```
-
-```python Per-Agent
-agent = client.agents.create(
- tools=["fetch_webpage"],
- tool_env_vars={
- "EXA_API_KEY": "your_exa_api_key"
- }
-)
-```
-
-
-## Common Patterns
-
-### Documentation Reader
-```python
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["fetch_webpage", "web_search"],
- memory_blocks=[{
- "label": "persona",
- "value": "I search for documentation with web_search and read it with fetch_webpage."
- }]
-)
-```
-
-### Research Assistant
-```python
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["fetch_webpage", "archival_memory_insert"],
- memory_blocks=[{
- "label": "persona",
- "value": "I fetch articles and store key insights in archival memory for later reference."
- }]
-)
-```
-
-### Content Summarizer
-```python
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["fetch_webpage"],
- memory_blocks=[{
- "label": "persona",
- "value": "I fetch webpages and provide summaries of their content."
- }]
-)
-```
-
-## When to Use
-
-| Use Case | Tool | Why |
-|----------|------|-----|
-| Read specific webpage | `fetch_webpage` | Direct URL access |
-| Find webpages to read | `web_search` | Discovery first |
-| Read + search in one | `web_search` with `include_text=true` | Combined operation |
-| Multiple pages | `fetch_webpage` | Iterate over URLs |
-
-## Related Documentation
-
-- [Utilities Overview](/guides/agents/prebuilt-tools)
-- [Web Search](/guides/agents/web-search)
-- [Run Code](/guides/agents/run-code)
-- [Custom Tools](/guides/agents/custom-tools)
-- [Tool Variables](/guides/agents/tool-variables)
diff --git a/fern/pages/agents/human_in_the_loop.mdx b/fern/pages/agents/human_in_the_loop.mdx
deleted file mode 100644
index 23aac132..00000000
--- a/fern/pages/agents/human_in_the_loop.mdx
+++ /dev/null
@@ -1,694 +0,0 @@
----
-title: Human-in-the-Loop
-slug: guides/agents/human-in-the-loop
-subtitle: How to integrate human-in-the-loop workflows for tool approval
----
-
-Human-in-the-loop (HITL) workflows allow you to maintain control over critical agent actions by requiring human approval before executing certain tools. This is essential for operations that could have significant consequences, such as database modifications, financial transactions, or external API calls with cost implications.
-
-```mermaid
-flowchart LR
- Agent[Agent] -->|Calls Tool| Check{Requires
Approval?}
- Check -->|No| Execute[Execute Tool]
- Check -->|Yes| Request[Request Approval]
- Request --> Human[Human Review]
- Human -->|Approve| Execute
- Human -->|Deny| Error[Return Error]
- Execute --> Result[Return Result]
- Error --> Agent
- Result --> Agent
-```
-
-## Overview
-
-When a tool is marked as requiring approval, the agent will pause execution and wait for human approval or denial before proceeding. This creates a checkpoint in the agent's workflow where human judgment can be applied. The approval workflow is designed to be non-blocking and supports both synchronous and streaming message interfaces, making it suitable for interactive applications as well as batch processing systems.
-
-### Key Benefits
-
-- **Risk Mitigation**: Prevent unintended actions in production environments
-- **Cost Control**: Review expensive operations before execution
-- **Compliance**: Ensure human oversight for regulated operations
-- **Quality Assurance**: Validate agent decisions before critical actions
-
-### How It Works
-
-The approval workflow follows a clear sequence of steps that ensures human oversight at critical decision points:
-
-1. **Tool Configuration**: Mark specific tools as requiring approval either globally (default for all agents) or per-agent
-2. **Execution Pause**: When the agent attempts to call a protected tool, it immediately pauses and returns an approval request message
-3. **Human Review**: The approval request includes the tool name, arguments, and context, allowing you to make an informed decision
-4. **Approval/Denial**: Send an approval response to either execute the tool or provide feedback for the agent to adjust its approach
-5. **Continuation**: The agent receives the tool result (on approval) or an error message (on denial) and continues processing
-
-
-## Best Practices
-
-Following these best practices will help you implement effective human-in-the-loop workflows while maintaining a good user experience and system performance.
-
-### 1. Selective Tool Marking
-
-Not every tool needs human approval. Be strategic about which tools require oversight to avoid workflow bottlenecks while maintaining necessary controls:
-
-**Tools that typically require approval:**
-- Database write operations (INSERT, UPDATE, DELETE)
-- External API calls with financial implications
-- File system modifications or deletions
-- Communication tools (email, SMS, notifications)
-- System configuration changes
-- Third-party service integrations with rate limits
-
-### 2. Clear Denial Reasons
-
-When denying a request, your feedback directly influences how the agent adjusts its approach. Provide specific, actionable guidance rather than vague rejections:
-
-```python
-# Good: Specific and actionable
-"reason": "Use read-only query first to verify the data before deletion"
-
-# Bad: Too vague
-"reason": "Don't do that"
-```
-
-The agent will use your denial reason to reformulate its approach, so the more specific you are, the better the agent can adapt.
-
-## Setting Up Approval Requirements
-
-There are two methods for configuring tool approval requirements, each suited for different use cases. Choose the approach that best fits your security model and operational needs.
-
-### Method 1: Create/Upsert Tool with Default Approval Requirement
-
-Set approval requirements at the tool level when creating or upserting a tool. This approach ensures consistent security policies across all agents that use the tool. The `default_requires_approval` flag will be applied to all future agent-tool attachments:
-
-
-```curl curl maxLines=50
-curl --request POST \
- --url https://api.letta.com/v1/tools \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "name": "sensitive_operation",
- "default_requires_approval": true,
- "json_schema": {
- "type": "function",
- "function": {
- "name": "sensitive_operation",
- "parameters": {...}
- }
- },
- "source_code": "def sensitive_operation(...): ..."
- }'
-
-# All agents using this tool will require approval
-curl --request POST \
- --url https://api.letta.com/v1/agents \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "tools": ["sensitive_operation"],
- // ... other configuration
- }'
-```
-```python python maxLines=50
-# Create a tool that requires approval by default
-approval_tool = client.tools.upsert_from_function(
- func=sensitive_operation,
- default_requires_approval=True,
-)
-
-# All agents using this tool will require approval
-agent = client.agents.create(
- tools=['sensitive_operation'],
- # ... other configuration
-)
-```
-```typescript TypeScript maxLines=50
-// Create a tool that requires approval by default
-const approvalTool = await client.tools.upsert({
- name: "sensitive_operation",
- defaultRequiresApproval: true,
- jsonSchema: {
- type: "function",
- function: {
- name: "sensitive_operation",
- parameters: {...}
- }
- },
- sourceCode: "def sensitive_operation(...): ..."
-});
-
-// All agents using this tool will require approval
-const agent = await client.agents.create({
- tools: ["sensitive_operation"],
- // ... other configuration
-});
-```
-
-
-### Method 2: Modify Existing Tool with Default Approval Requirement
-
-
-Modifying the tool-level setting will not retroactively apply to existing agent-tool attachments - it only sets the default for future attachments. This means that if the tool is already attached to an agent, the agent will continue using the tool without approval. To modify an existing agent-tool attachment, refer to Method 3 below.
-
-
-For an already existing tool, you can modify the tool to set approval requirements on future agent-tool attachments. The `default_requires_approval` flag will be applied to all future agent-tool attachments:
-
-
-```curl curl maxLines=50
-curl --request PATCH \
- --url https://api.letta.com/v1/tools/$TOOL_ID \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "default_requires_approval": true
- }'
-
-# All agents using this tool will require approval
-curl --request POST \
- --url https://api.letta.com/v1/agents \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "tools": ["sensitive_operation"],
- // ... other configuration
- }'
-```
-```python python maxLines=50
-# Create a tool that requires approval by default
-approval_tool = client.tools.modify(
- tool_id=sensitive_operation.id,
- default_requires_approval=True,
-)
-
-# All agents using this tool will require approval
-agent = client.agents.create(
- tools=['sensitive_operation'],
- # ... other configuration
-)
-```
-```typescript TypeScript maxLines=50
-// Create a tool that requires approval by default
-const approvalTool = await client.tools.modify({
- tool_id=sensitive_operation.id,
- defaultRequiresApproval: true,
-});
-
-// All agents using this tool will require approval
-const agent = await client.agents.create({
- tools: ["sensitive_operation"],
- // ... other configuration
-});
-```
-
-
-### Method 3: Per-Agent Tool Approval
-
-Configure approval requirements for specific agent-tool combinations, allowing fine-grained control over individual agent behaviors. This method is particularly useful for:
-
-- **Trusted agents**: Remove approval requirements for well-tested, reliable agents
-- **Progressive autonomy**: Gradually reduce approval requirements as agents prove reliable
-- **Override defaults**: Change the approval setting for tools already attached to an agent
-
-Use the following endpoints to modify approval settings for existing agent-tool relationships:
-
-
-```curl curl maxLines=50
-curl --request PATCH \
- --url https://api.letta.com/v1/agents/$AGENT_ID/tools/$TOOL_NAME/approval \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "requires_approval": true
- }'
-```
-```python python maxLines=50
-# Modify approval requirement for a specific agent
-client.agents.tools.modify_approval(
- agent_id=agent.id,
- tool_name="database_write",
- requires_approval=True,
-)
-
-# Check current approval settings
-tools = client.agents.tools.list(agent_id=agent.id)
-for tool in tools:
- print(f"{tool.name}: requires_approval={tool.requires_approval}")
-```
-```typescript TypeScript maxLines=50
-// Modify approval requirement for a specific agent
-await client.agents.tools.modifyApproval({
- agentId: agent.id,
- toolName: "database_write",
- requiresApproval: true,
-});
-
-// Check current approval settings
-const tools = await client.agents.tools.list({
- agentId: agent.id,
-});
-for (const tool of tools) {
- console.log(`${tool.name}: requires_approval=${tool.requiresApproval}`);
-}
-```
-
-
-## Handling Approval Requests
-
-### Step 1: Agent Requests Approval
-
-When the agent attempts to call a tool that requires approval, execution immediately pauses. The agent returns a special approval request message containing:
-
-- **Tool name**: The specific tool being called
-- **Arguments**: The exact parameters the agent intends to pass
-- **Tool call ID**: A unique identifier for tracking this specific call
-- **Message ID**: The approval request ID needed for your response
-- **Stop reason**: Set to `"requires_approval"` to indicate the pause state
-
-This format matches the ToolCallMessage format intentionally, so that we can handle approval requests the same way we handle tool calls. Here's what an approval request looks like in practice:
-
-
-```curl curl maxLines=50
-curl --request POST \
- --url https://api.letta.com/v1/agents/$AGENT_ID/messages \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "messages": [{
- "role": "user",
- "content": "Delete all test data from the database"
- }]
- }'
-
-# Response includes approval request
-{
- "messages": [
- {
- "message_type": "reasoning_message",
- "reasoning": "I need to delete test data from the database..."
- },
- {
- "message_type": "approval_request_message",
- "id": "message-abc123",
- "tool_call": {
- "name": "database_write",
- "arguments": "{\"query\": \"DELETE FROM test_data\"}",
- "tool_call_id": "tool-xyz789"
- }
- }
- ],
- "stop_reason": "requires_approval"
-}
-```
-```python python maxLines=50
-response = client.agents.messages.create(
- agent_id=agent.id,
- messages=[{
- "role": "user",
- "content": "Delete all test data from the database"
- }]
-)
-
-# Response includes approval request
-{
- "messages": [
- {
- "message_type": "reasoning_message",
- "reasoning": "I need to delete test data from the database..."
- },
- {
- "message_type": "approval_request_message",
- "id": "message-abc123",
- "tool_call": {
- "name": "database_write",
- "arguments": "{\"query\": \"DELETE FROM test_data\"}",
- "tool_call_id": "tool-xyz789"
- }
- }
- ],
- "stop_reason": "requires_approval"
-}
-```
-```typescript TypeScript maxLines=50
-const response = await client.agents.messages.create({
- agentId: agent.id,
- requestBody: {
- messages: [{
- role: "user",
- content: "Delete all test data from the database"
- }]
- }
-});
-
-// Response includes approval request
-{
- "messages": [
- {
- "message_type": "reasoning_message",
- "reasoning": "I need to delete test data from the database..."
- },
- {
- "message_type": "approval_request_message",
- "id": "message-abc123",
- "tool_call": {
- "name": "database_write",
- "arguments": "{\"query\": \"DELETE FROM test_data\"}",
- "tool_call_id": "tool-xyz789"
- }
- }
- ],
- "stop_reason": "requires_approval"
-}
-```
-
-
-
-
-### Step 2: Review and Respond
-
-Once you receive an approval request, you have two options: approve the tool execution or deny it with guidance. The agent will remain paused until it receives your response.
-
- While an approval is pending, the agent cannot process any other messages - you must resolve the approval request first.
-
-#### Approving the Request
-
-To approve a tool call, send an approval message with `approve: true` and the approval request ID. The agent will immediately execute the tool and continue processing:
-
-
-```curl curl maxLines=50
-curl --request POST \
- --url https://api.letta.com/v1/agents/$AGENT_ID/messages \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "messages": [{
- "type": "approval",
- "approvals": [{
- "approve": true,
- "tool_call_id": "tool-xyz789"
- }]
- }]
- }'
-
-# Response continues with tool execution
-{
- "messages": [
- {
- "message_type": "tool_return_message",
- "status": "success",
- "tool_return": "Deleted 1,234 test records"
- },
- {
- "message_type": "reasoning_message",
- "reasoning": "I was able to delete the test data. Let me inform the user."
- },
- {
- "message_type": "assistant_message",
- "content": "I've successfully deleted 1,234 test records from the database."
- }
- ],
- "stop_reason": "end_turn"
-}
-```
-```python python maxLines=50
-# Approve the tool call
-response = client.agents.messages.create(
- agent_id=agent.id,
- messages=[{
- "type": "approval",
- "approvals": [{
- "approve": True,
- "tool_call_id": "tool-xyz789"
- }]
- }]
-)
-
-# Response continues with tool execution
-{
- "messages": [
- {
- "message_type": "tool_return_message",
- "status": "success",
- "tool_return": "Deleted 1,234 test records"
- },
- {
- "message_type": "reasoning_message",
- "reasoning": "I was able to delete the test data. Let me inform the user."
- },
- {
- "message_type": "assistant_message",
- "content": "I've successfully deleted 1,234 test records from the database."
- }
- ],
- "stop_reason": "end_turn"
-}
-```
-```typescript TypeScript maxLines=50
-// Approve the tool call
-const response = await client.agents.messages.create({
- agentId: agent.id,
- requestBody: {
- messages: [{
- type: "approval",
- approvals: [{
- approve: true,
- tool_call_id: "tool-xyz789"
- }]
- }]
- }
-});
-
-// Response continues with tool execution
-{
- "messages": [
- {
- "message_type": "tool_return_message",
- "status": "success",
- "tool_return": "Deleted 1,234 test records"
- },
- {
- "message_type": "reasoning_message",
- "reasoning": "I was able to delete the test data. Let me inform the user."
- },
- {
- "message_type": "assistant_message",
- "content": "I've successfully deleted 1,234 test records from the database."
- }
- ],
- "stop_reason": "end_turn"
-}
-```
-
-
-#### Denying with Guidance
-
-When denying a tool call, you can provide a reason that helps the agent understand how to adjust its approach. The agent will receive an error response and can use your feedback to reformulate its strategy. This is particularly useful for guiding the agent toward safer or more appropriate actions:
-
-
-```curl curl maxLines=50
-curl --request POST \
- --url https://api.letta.com/v1/agents/$AGENT_ID/messages \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "messages": [{
- "type": "approval",
- "approvals": [{
- "approve": false,
- "tool_call_id": "tool-xyz789",
- "reason": "Only delete records older than 30 days, not all test data"
- }]
- }]
- }'
-
-# Response shows agent adjusting based on feedback
-{
- "messages": [
- {
- "message_type": "tool_return_message",
- "status": "error",
- "tool_return": "Error: request denied. Reason: Only delete records older than 30 days, not all test data"
- },
- {
- "message_type": "reasoning_message",
- "reasoning": "I need to modify my query to only delete old records..."
- },
- {
- "message_type": "tool_call_message",
- "tool_call": {
- "name": "database_write",
- "arguments": "{\"query\": \"DELETE FROM test_data WHERE created_at < NOW() - INTERVAL 30 DAY\"}"
- }
- }
- ],
- "stop_reason": "requires_approval"
-}
-```
-```python python maxLines=50
-# Deny with explanation
-response = client.agents.messages.create(
- agent_id=agent.id,
- messages=[{
- "type": "approval",
- "approvals": [{
- "approve": False,
- "tool_call_id": "tool-xyz789",
- "reason": "Only delete records older than 30 days, not all test data"
- }]
- }]
-)
-
-# Response shows agent adjusting based on feedback
-{
- "messages": [
- {
- "message_type": "tool_return_message",
- "status": "error",
- "tool_return": "Error: request denied. Reason: Only delete records older than 30 days, not all test data"
- },
- {
- "message_type": "reasoning_message",
- "reasoning": "I need to modify my query to only delete old records..."
- },
- {
- "message_type": "tool_call_message",
- "tool_call": {
- "name": "database_write",
- "arguments": "{\"query\": \"DELETE FROM test_data WHERE created_at < NOW() - INTERVAL 30 DAY\"}"
- }
- }
- ],
- "stop_reason": "requires_approval"
-}
-```
-```typescript TypeScript maxLines=50
-// Deny with explanation
-const response = await client.agents.messages.create({
- agentId: agent.id,
- requestBody: {
- messages: [{
- type: "approval",
- approvals: [{
- approve: false,
- tool_call_id: "tool-xyz789",
- reason: "Only delete records older than 30 days, not all test data"
- }]
- }]
- }
-});
-
-// Response shows agent adjusting based on feedback
-{
- "messages": [
- {
- "message_type": "tool_return_message",
- "status": "error",
- "tool_return": "Error: request denied. Reason: Only delete records older than 30 days, not all test data"
- },
- {
- "message_type": "reasoning_message",
- "reasoning": "I need to modify my query to only delete old records..."
- },
- {
- "message_type": "tool_call_message",
- "tool_call": {
- "name": "database_write",
- "arguments": "{\"query\": \"DELETE FROM test_data WHERE created_at < NOW() - INTERVAL 30 DAY\"}"
- }
- }
- ],
- "stop_reason": "requires_approval"
-}
-```
-
-
-### Streaming + Background Mode
-
-For streaming clients using background mode, approvals are best handled via `agents.messages.createStream(..., background: true)`. The approval response may include the `tool_return_message` on the approval stream itself, and follow‑up reasoning/assistant messages can be read by resuming that stream’s `run_id`.
-
-
-Do not assume the `tool_return_message` will repeat after you resume. Treat the one on the approval stream as the source of truth, then resume to continue reading subsequent tokens.
-
-
-
-```curl curl maxLines=70
-# Approve in background after receiving approval_request_message
-curl --request POST --url https://api.letta.com/v1/agents/$AGENT_ID/messages/stream --header 'Content-Type: application/json' --data '{
- "messages": [{"type": "approval", "approve": true, "approval_request_id": "message-abc"}],
- "stream_tokens": true,
- "background": true
-}'
-
-# Example approval stream output (tool result arrives here):
-data: {"run_id":"run-new","seq_id":0,"message_type":"tool_return_message","status":"success","tool_return":"..."}
-
-# Continue by resuming the approval stream's run
-curl --request GET --url https://api.letta.com/v1/runs/$RUN_ID/stream --header 'Accept: text/event-stream' --data '{
- "starting_after": 0
-}'
-```
-```python python maxLines=70
-# Receive an approval_request_message, then approve in background
-approve = client.agents.messages.create_stream(
- agent_id=agent.id,
- messages=[{"type": "approval", "approvals": [{"approve": True, "tool_call_id": "tool-xyz789"}]}],
- stream_tokens=True,
- background=True,
-)
-
-run_id = None
-last_seq = 0
-for chunk in approve:
- if hasattr(chunk, "run_id") and hasattr(chunk, "seq_id"):
- run_id = chunk.run_id
- last_seq = chunk.seq_id
- if getattr(chunk, "message_type", None) == "tool_return_message":
- # Tool result arrives here on the approval stream
- break
-
-# Continue consuming output by resuming the background run
-if run_id:
- for chunk in client.runs.stream(run_id, starting_after=last_seq):
- print(chunk)
-```
-```typescript TypeScript maxLines=70
-// Receive an approval_request_message, then approve in background
-const approve = await client.agents.messages.createStream({
- agentId: agent.id,
- requestBody: {
- messages: [{ type: "approval", approvals: [{ approve: true, tool_call_id: "tool-xyz789" }] }],
- streamTokens: true,
- background: true,
- }
-});
-
-let runId: string | null = null;
-let lastSeq = 0;
-for await (const chunk of approve) {
- if (chunk.run_id && chunk.seq_id) { runId = chunk.run_id; lastSeq = chunk.seq_id; }
- if (chunk.message_type === "tool_return_message") {
- // Tool result arrives here on the approval stream
- break;
- }
-}
-
-// Continue consuming output by resuming the background run
-if (runId) {
- const resume = await client.runs.stream(runId, { startingAfter: lastSeq });
- for await (const chunk of resume) {
- console.log(chunk);
- }
-}
-```
-
-
-
-
-
-**Run switching in background mode:** Approvals are separate background requests and create a new `run_id`. Save the approval stream cursor and resume that run. The original paused run will not deliver the tool result — do not wait for the tool return there.
-
-
-See [background mode](/guides/agents/long-running) for resumption patterns.
-### IDs and UI Triggers
-
-- **approval_request_id**: This field is now deprecated, but it is still used for backwards compatibility. Used `approval_request_message.id`.
-- **tool_call_id**: Always send approvals/denials using the `tool_call_id` from the `ApprovalRequestMessage`.
-- **UI trigger**: Open the approval UI on `approval_request_message` only; do not derive UI from `stop_reason`.
diff --git a/fern/pages/agents/json_mode.mdx b/fern/pages/agents/json_mode.mdx
deleted file mode 100644
index bf50ef69..00000000
--- a/fern/pages/agents/json_mode.mdx
+++ /dev/null
@@ -1,460 +0,0 @@
----
-title: JSON Mode & Structured Output
-subtitle: Get structured JSON responses from your Letta agents
-slug: guides/agents/json-mode
----
-
-Letta provides two ways to get structured JSON output from agents: **Structured Generation through Tools** (recommended) and the `response_format` parameter.
-
-## Quick Comparison
-
-
-**Recommended**: Use **Structured Generation through Tools** - works with all providers (Anthropic, OpenAI, Google, etc.) and integrates naturally with Letta's tool-calling architecture.
-
-
-
-**Structured Generation through Tools**:
-- ✅ Universal provider compatibility
-- ✅ Both reasoning AND structured output
-- ✅ Per-message control
-- ✅ Works even as "dummy tool" for pure formatting
-
-
-
-**`response_format` parameter**:
-- ⚠️ OpenAI-compatible providers only (NOT Anthropic)
-- ⚠️ Persistent agent state (affects all future responses)
-
-- ✅ Built-in provider schema enforcement
-
-
-## Structured Generation through Tools (Recommended)
-
-Create a tool that defines your desired response format. The tool arguments become your structured data, and you can extract them from the tool call.
-
-### Creating a Structured Generation Tool
-
-
-```typescript TypeScript maxLines=100
-import { LettaClient } from '@letta-ai/letta-client'
-
-// Create client connected to Letta Cloud
-const client = new LettaClient({ token: process.env.LETTA_API_KEY });
-
-// First create the tool
-const toolCode = `def generate_rank(rank: int, reason: str):
- """Generate a ranking with explanation.
-
- Args:
- rank (int): The numerical rank from 1-10.
- reason (str): The reasoning behind the rank.
- """
- print("Rank generated")
- return`;
-
-const tool = await client.tools.create({
- sourceCode: toolCode,
- sourceType: "python"
-});
-
-// Create agent with the structured generation tool
-const agentState = await client.agents.create({
- model: "openai/gpt-4o-mini",
- memoryBlocks: [
- {
- label: "human",
- value: "The human's name is Chad. They are a food enthusiast who enjoys trying different cuisines."
- },
- {
- label: "persona",
- value: "I am a helpful food critic assistant. I provide detailed rankings and reviews of different foods and restaurants."
- }
- ],
- toolIds: [tool.id]
-});
-```
-
-```python title="python" maxLines=100
-from letta_client import Letta
-
-# Create client connected to Letta Cloud
-import os
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-def generate_rank(rank: int, reason: str):
- """Generate a ranking with explanation.
-
- Args:
- rank (int): The numerical rank from 1-10.
- reason (str): The reasoning behind the rank.
- """
- print("Rank generated")
- return
-
-# Create the tool
-tool = client.tools.create(func=generate_rank)
-
-# Create agent with the structured generation tool
-agent_state = client.agents.create(
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "human",
- "value": "The human's name is Chad. They are a food enthusiast who enjoys trying different cuisines."
- },
- {
- "label": "persona",
- "value": "I am a helpful food critic assistant. I provide detailed rankings and reviews of different foods and restaurants."
- }
- ],
- tool_ids=[tool.id]
-)
-```
-
-
-### Using the Structured Generation Tool
-
-
-```typescript TypeScript maxLines=100
-// Send message and instruct agent to use the tool
-const response = await client.agents.messages.create(
- agentState.id, {
- messages: [
- {
- role: "user",
- content: "How do you rank sushi as a food? Please use the generate_rank tool to provide your response."
- }
- ]
- }
-);
-
-// Extract structured data from tool call
-for (const message of response.messages) {
- if (message.messageType === "tool_call_message") {
- const args = JSON.parse(message.toolCall.arguments);
- console.log(`Rank: ${args.rank}`);
- console.log(`Reason: ${args.reason}`);
- }
-}
-
-// Example output:
-// Rank: 8
-// Reason: Sushi is a highly regarded cuisine known for its fresh ingredients...
-```
-
-```python title="python" maxLines=100
-# Send message and instruct agent to use the tool
-response = client.agents.messages.create(
- agent_id=agent_state.id,
- messages=[
- {
- "role": "user",
- "content": "How do you rank sushi as a food? Please use the generate_rank tool to provide your response."
- }
- ]
-)
-
-# Extract structured data from tool call
-for message in response.messages:
- if message.message_type == "tool_call_message":
- import json
- args = json.loads(message.tool_call.arguments)
- rank = args["rank"]
- reason = args["reason"]
- print(f"Rank: {rank}")
- print(f"Reason: {reason}")
-
-# Example output:
-# Rank: 8
-# Reason: Sushi is a highly regarded cuisine known for its fresh ingredients...
-```
-
-
-The agent will call the tool, and you can extract the structured arguments:
-
-```json
-{
- "rank": 8,
- "reason": "Sushi is a highly regarded cuisine known for its fresh ingredients, artistic presentation, and cultural significance."
-}
-```
-
-## Using `response_format` for Provider-Native JSON Mode
-
-The `response_format` parameter enables structured output/JSON mode from LLM providers that support it. This approach is fundamentally different from tools because **`response_format` becomes a persistent part of the agent's state** - once set, all future responses from that agent will follow the format until explicitly changed.
-
-Under the hood, `response_format` constrains the agent's assistant messages to follow the specified schema, but it doesn't affect tools - those continue to work normally with their original schemas.
-
-
-**Requirements for `response_format`:**
-- Only works with providers that support structured outputs (like OpenAI) - NOT Anthropic or other providers
-
-
-
-### Basic JSON Mode
-
-
-```typescript TypeScript maxLines=100
-import { LettaClient } from '@letta-ai/letta-client'
-
-// Create client (Letta Cloud)
-const client = new LettaClient({ token: "LETTA_API_KEY" });
-
-// Create agent with basic JSON mode (OpenAI/compatible providers only)
-const agentState = await client.agents.create({
- model: "openai/gpt-4o-mini",
- memoryBlocks: [
- {
- label: "human",
- value: "The human's name is Chad. They work as a data analyst and prefer clear, organized information."
- },
- {
- label: "persona",
- value: "I am a helpful assistant who provides clear and well-organized responses."
- }
- ],
- responseFormat: { type: "json_object" }
-});
-
-// Send message expecting JSON response
-const response = await client.agents.messages.create(
- agentState.id, {
- messages: [
- {
- role: "user",
- content: "How do you rank sushi as a food? Please respond in JSON format with rank and reason fields."
- }
- ]
- }
-);
-
-for (const message of response.messages) {
- console.log(message);
-}
-```
-
-```python title="python" maxLines=100
-from letta_client import Letta
-
-# Create client (Letta Cloud)
-client = Letta(token="LETTA_API_KEY")
-
-# Create agent with basic JSON mode (OpenAI/compatible providers only)
-agent_state = client.agents.create(
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "human",
- "value": "The human's name is Chad. They work as a data analyst and prefer clear, organized information."
- },
- {
- "label": "persona",
- "value": "I am a helpful assistant who provides clear and well-organized responses."
- }
- ],
- response_format={"type": "json_object"}
-)
-
-# Send message expecting JSON response
-response = client.agents.messages.create(
- agent_id=agent_state.id,
- messages=[
- {
- "role": "user",
- "content": "How do you rank sushi as a food? Please respond in JSON format with rank and reason fields."
- }
- ]
-)
-
-for message in response.messages:
- print(message)
-```
-
-
-### Advanced JSON Schema Mode
-
-For more precise control, you can use OpenAI's `json_schema` mode with strict validation:
-
-
-```typescript TypeScript maxLines=100
-import { LettaClient } from '@letta-ai/letta-client'
-
-const client = new LettaClient({ token: "LETTA_API_KEY" });
-
-// Define structured schema (from OpenAI structured outputs guide)
-const responseFormat = {
- type: "json_schema",
- jsonSchema: {
- name: "food_ranking",
- schema: {
- type: "object",
- properties: {
- rank: {
- type: "integer",
- minimum: 1,
- maximum: 10
- },
- reason: {
- type: "string"
- },
- categories: {
- type: "array",
- items: {
- type: "object",
- properties: {
- name: { type: "string" },
- score: { type: "integer" }
- },
- required: ["name", "score"],
- additionalProperties: false
- }
- }
- },
- required: ["rank", "reason", "categories"],
- additionalProperties: false
- },
- strict: true
- }
-};
-
-// Create agent
-const agentState = await client.agents.create({
- model: "openai/gpt-4o-mini",
- memoryBlocks: []
-});
-
-// Update agent with response format
-const updatedAgent = await client.agents.update(
- agentState.id,
- { responseFormat }
-);
-
-// Send message
-const response = await client.agents.messages.create(
- agentState.id, {
- messages: [
- { role: "user", content: "How do you rank sushi? Include categories for taste, presentation, and value." }
- ]
- }
-);
-
-for (const message of response.messages) {
- console.log(message);
-}
-```
-
-```python title="python" maxLines=100
-from letta_client import Letta
-
-client = Letta(token="LETTA_API_KEY")
-
-# Define structured schema (from OpenAI structured outputs guide)
-response_format = {
- "type": "json_schema",
- "json_schema": {
- "name": "food_ranking",
- "schema": {
- "type": "object",
- "properties": {
- "rank": {
- "type": "integer",
- "minimum": 1,
- "maximum": 10
- },
- "reason": {
- "type": "string"
- },
- "categories": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "name": { "type": "string" },
- "score": { "type": "integer" }
- },
- "required": ["name", "score"],
- "additionalProperties": False
- }
- }
- },
- "required": ["rank", "reason", "categories"],
- "additionalProperties": False
- },
- "strict": True
- }
-}
-
-# Create agent
-agent_state = client.agents.create(
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[]
-)
-
-# Update agent with response format
-agent_state = client.agents.update(
- agent_id=agent_state.id,
- response_format=response_format
-)
-
-# Send message
-response = client.agents.messages.create(
- agent_id=agent_state.id,
- messages=[
- {"role": "user", "content": "How do you rank sushi? Include categories for taste, presentation, and value."}
- ]
-)
-
-for message in response.messages:
- print(message)
-```
-
-
-With structured JSON schema, the agent's response will be strictly validated:
-
-```json
-{
- "rank": 8,
- "reason": "Sushi is highly regarded for its fresh ingredients and artful presentation",
- "categories": [
- {"name": "taste", "score": 9},
- {"name": "presentation", "score": 10},
- {"name": "value", "score": 6}
- ]
-}
-```
-
-
-## Updating Agent Response Format
-
-You can update an existing agent's response format:
-
-
-```typescript TypeScript maxLines=100
-// Update agent to use JSON mode (OpenAI/compatible only)
-await client.agents.update(agentState.id, {
- responseFormat: { type: "json_object" }
-});
-
-// Or remove JSON mode
-await client.agents.update(agentState.id, {
- responseFormat: null
-});
-```
-
-```python title="python" maxLines=100
-# Update agent to use JSON mode (OpenAI/compatible only)
-client.agents.update(
- agent_id=agent_state.id,
- response_format={"type": "json_object"}
-)
-
-# Or remove JSON mode
-client.agents.update(
- agent_id=agent_state.id,
- response_format=None
-)
-```
-
diff --git a/fern/pages/agents/long_running.mdx b/fern/pages/agents/long_running.mdx
deleted file mode 100644
index 524b6e3e..00000000
--- a/fern/pages/agents/long_running.mdx
+++ /dev/null
@@ -1,602 +0,0 @@
----
-title: Long-Running Executions
-slug: guides/agents/long-running
-subtitle: How to handle long-running agent executions
----
-
-When agents need to execute multiple tool calls or perform complex operations (like deep research, data analysis, or multi-step workflows), processing time can vary significantly.
-
-Letta supports various ways to handle long-running agents, so you can choose the approach that best fits your use case:
-
-| Use Case | Duration | Recommendedation | Key Benefits |
-|----------|----------|---------------------|-------------|
-| Few-step invocations | < 1 minute | [Standard streaming](/guides/agents/streaming) | Simplest approach |
-| Variable length runs | 1-10 minutes | **Background mode** (Keepalive + Timeout as a second choice) | Easy way to reduce timeouts |
-| Deep research | 10+ minutes | **Background mode**, or async polling | Survives disconnects, resumable streams |
-| Batch jobs | Any | **Async polling** | Fire-and-forget, check results later |
-
-## Option 1: Background Mode with Resumable Streaming
-
-
-**Best for:** Operations exceeding 10 minutes, unreliable network connections, or critical workflows that must complete regardless of client connectivity.
-
-**Trade-off:** Slightly higher latency to first token due to background task initialization.
-
-
-Background mode decouples agent execution from your client connection. The agent processes your request on the server while streaming results to a persistent store, allowing you to reconnect and resume from any point — even if your application crashes or network fails.
-
-
-```curl curl maxLines=50
-curl --request POST \
- --url https://api.letta.com/v1/agents/$AGENT_ID/messages/stream \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "messages": [
- {
- "role": "user",
- "content": "Run comprehensive analysis on this dataset"
- }
- ],
- "stream_tokens": true,
- "background": true
-}'
-
-# Response stream includes run_id and seq_id for each chunk:
-data: {"run_id":"run-123","seq_id":0,"message_type":"reasoning_message","reasoning":"Analyzing"}
-data: {"run_id":"run-123","seq_id":1,"message_type":"reasoning_message","reasoning":" the dataset"}
-data: {"run_id":"run-123","seq_id":2,"message_type":"tool_call","tool_call":{...}}
-# ... stream continues
-
-# Step 2: If disconnected, resume from last received seq_id
-curl --request GET \
- --url https://api.letta.com/v1/runs/$RUN_ID/stream \
- --header 'Accept: text/event-stream' \
- --data '{
- "starting_after": 57
-}'
-```
-```python python maxLines=50
-stream = client.agents.messages.create_stream(
- agent_id=agent_state.id,
- messages=[
- {
- "role": "user",
- "content": "Run comprehensive analysis on this dataset"
- }
- ],
- stream_tokens=True,
- background=True,
-)
-run_id = None
-last_seq_id = None
-for chunk in stream:
- if hasattr(chunk, "run_id") and hasattr(chunk, "seq_id"):
- run_id = chunk.run_id # Save this to reconnect if your connection drops
- last_seq_id = chunk.seq_id # Save this as your resumption point for cursor-based pagination
- print(chunk)
-
-# If disconnected, resume from last received seq_id:
-for chunk in client.runs.stream(run_id, starting_after=last_seq_id):
- print(chunk)
-```
-```typescript TypeScript maxLines=50
-const stream = await client.agents.messages.createStream({
- agentId: agentState.id,
- requestBody: {
- messages: [
- {
- role: "user",
- content: "Run comprehensive analysis on this dataset"
- }
- ],
- streamTokens: true,
- background: true,
- }
-});
-
-let runId = null;
-let lastSeqId = null;
-for await (const chunk of stream) {
- if (chunk.run_id && chunk.seq_id) {
- runId = chunk.run_id; // Save this to reconnect if your connection drops
- lastSeqId = chunk.seq_id; // Save this as your resumption point for cursor-based pagination
- }
- console.log(chunk);
-}
-
-// If disconnected, resume from last received seq_id
-for await (const chunk of client.runs.stream(runId, {startingAfter: lastSeqId})) {
- console.log(chunk);
-}
-```
-```python python maxLines=60
-# 1) Start background stream and capture approval request
-stream = client.agents.messages.create_stream(
- agent_id=agent.id,
- messages=[{"role": "user", "content": "Do a sensitive operation"}],
- stream_tokens=True,
- background=True,
-)
-
-approval_request_id = None
-orig_run_id = None
-last_seq_id = 0
-for chunk in stream:
- if hasattr(chunk, "run_id") and hasattr(chunk, "seq_id"):
- orig_run_id = chunk.run_id
- last_seq_id = chunk.seq_id
- if getattr(chunk, "message_type", None) == "approval_request_message":
- approval_request_id = chunk.id
- break
-
-# 2) Approve in background; capture the approval stream cursor (this creates a new run)
-approve = client.agents.messages.create_stream(
- agent_id=agent.id,
- messages=[{"type": "approval", "approve": True, "approval_request_id": approval_request_id}],
- stream_tokens=True,
- background=True,
-)
-
-run_id = None
-approve_seq = 0
-for chunk in approve:
- if hasattr(chunk, "run_id") and hasattr(chunk, "seq_id"):
- run_id = chunk.run_id
- approve_seq = chunk.seq_id
- if getattr(chunk, "message_type", None) == "tool_return_message":
- # Tool result arrives here on the approval stream
- break
-
-# 3) Resume that run to read follow-up tokens
-for chunk in client.runs.stream(run_id, starting_after=approve_seq):
- print(chunk)
-```
-```typescript TypeScript maxLines=60
-// 1) Start background stream and capture approval request
-const stream = await client.agents.messages.createStream(
- agent.id, {
- messages: [{role: "user", content: "Do a sensitive operation"}],
- streamTokens: true,
- background: true,
- }
-);
-
-let approvalRequestId = null;
-let origRunId = null;
-let lastSeqId = 0;
-for await (const chunk of stream) {
- if (chunk.runId && chunk.seqId) {
- origRunId = chunk.runId;
- lastSeqId = chunk.seqId;
- }
- if (chunk.messageType === "approval_request_message") {
- approvalRequestId = chunk.id;
- break;
- }
-}
-
-// 2) Approve in background; capture the approval stream cursor (this creates a new run)
-const approveStream = await client.agents.messages.createStream(
- agent.id, {
- messages: [{type: "approval", approve: true, approvalRequestId}],
- streamTokens: true,
- background: true,
- }
-);
-
-let runId = null;
-let approveSeq = 0;
-for await (const chunk of approveStream) {
- if (chunk.runId && chunk.seqId) {
- runId = chunk.runId;
- approveSeq = chunk.seqId;
- }
- if (chunk.messageType === "tool_return_message") {
- // Tool result arrives here on the approval stream
- break;
- }
-}
-
-// 3) Resume that run to read follow-up tokens
-for await (const chunk of client.runs.stream(runId, {startingAfter: approveSeq})) {
- console.log(chunk);
-}
-```
-
-
-### HITL in Background Mode
-
-When [Human‑in‑the‑Loop (HITL) approval](/guides/agents/human-in-the-loop) is enabled for a tool, your background stream may pause and emit an `approval_request_message`. In background mode, send the approval via a separate background stream and capture that stream’s `run_id`/`seq_id`.
-
-
-Approval responses in background mode emit the `tool_return_message` on the approval stream itself (with a new `run_id`, different from the original stream). Save the approval stream cursor, then resume with `runs.stream` to consume subsequent reasoning/assistant messages.
-
-
-
-```curl curl maxLines=70
-# 1) Start background stream; capture approval request
-curl --request POST \
- --url https://api.letta.com/v1/agents/$AGENT_ID/messages/stream \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "messages": [{"role": "user", "content": "Do a sensitive operation"}],
- "stream_tokens": true,
- "background": true
-}'
-
-# Example stream output (approval request arrives):
-data: {"run_id":"run-abc","seq_id":0,"message_type":"reasoning_message","reasoning":"..."}
-data: {"run_id":"run-abc","seq_id":1,"message_type":"approval_request_message","id":"message-abc","tool_call":{"name":"sensitive_operation","arguments":"{...}","tool_call_id":"tool-xyz"}}
-
-# 2) Approve in background; capture approval stream cursor (this creates a new run)
-curl --request POST \
- --url https://api.letta.com/v1/agents/$AGENT_ID/messages/stream \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "messages": [{"type": "approval", "approve": true, "approval_request_id": "message-abc"}],
- "stream_tokens": true,
- "background": true
-}'
-
-# Example approval stream output (tool result arrives here):
-data: {"run_id":"run-new","seq_id":0,"message_type":"tool_return_message","status":"success","tool_return":"..."}
-
-# 3) Resume the approval stream's run to continue
-curl --request GET \
- --url https://api.letta.com/v1/runs/$RUN_ID/stream \
- --header 'Accept: text/event-stream' \
- --data '{
- "starting_after": 0
-}'
-```
-```python python maxLines=70
-# 1) Start background stream and capture approval request
-stream = client.agents.messages.create_stream(
- agent_id=agent.id,
- messages=[{"role": "user", "content": "Do a sensitive operation"}],
- stream_tokens=True,
- background=True,
-)
-
-approval_request_id = None
-orig_run_id = None
-last_seq_id = 0
-for chunk in stream:
- if hasattr(chunk, "run_id") and hasattr(chunk, "seq_id"):
- orig_run_id = chunk.run_id
- last_seq_id = chunk.seq_id
- if getattr(chunk, "message_type", None) == "approval_request_message":
- approval_request_id = chunk.id
- break
-
-# 2) Approve in background; capture the approval stream cursor (this creates a new run)
-approve = client.agents.messages.create_stream(
- agent_id=agent.id,
- messages=[{"type": "approval", "approve": True, "approval_request_id": approval_request_id}],
- stream_tokens=True,
- background=True,
-)
-
-run_id = None
-approve_seq = 0
-for chunk in approve:
- if hasattr(chunk, "run_id") and hasattr(chunk, "seq_id"):
- run_id = chunk.run_id
- approve_seq = chunk.seq_id
- if getattr(chunk, "message_type", None) == "tool_return_message":
- # Tool result arrives here on the approval stream
- break
-
-# 3) Resume that run to read follow-up tokens
-for chunk in client.runs.stream(run_id, starting_after=approve_seq):
- print(chunk)
-```
-```typescript TypeScript maxLines=70
-// 1) Start background stream and capture approval request
-const stream = await client.agents.messages.createStream({
- agentId: agent.id,
- requestBody: {
- messages: [{ role: "user", content: "Do a sensitive operation" }],
- streamTokens: true,
- background: true,
- }
-});
-
-let approvalRequestId: string | null = null;
-let origRunId: string | null = null;
-let lastSeqId = 0;
-for await (const chunk of stream) {
- if (chunk.run_id && chunk.seq_id) { origRunId = chunk.run_id; lastSeqId = chunk.seq_id; }
- if (chunk.message_type === "approval_request_message") {
- approvalRequestId = chunk.id; break;
- }
-}
-
-// 2) Approve in background; capture the approval stream cursor (this creates a new run)
-const approve = await client.agents.messages.createStream({
- agentId: agent.id,
- requestBody: {
- messages: [{ type: "approval", approve: true, approvalRequestId }],
- streamTokens: true,
- background: true,
- }
-});
-
-let runId: string | null = null;
-let approveSeq = 0;
-for await (const chunk of approve) {
- if (chunk.run_id && chunk.seq_id) { runId = chunk.run_id; approveSeq = chunk.seq_id; }
- if (chunk.message_type === "tool_return_message") {
- // Tool result arrives here on the approval stream
- break;
- }
-}
-
-// 3) Resume that run to read follow-up tokens
-const resume = await client.runs.stream(runId!, { startingAfter: approveSeq });
-for await (const chunk of resume) {
- console.log(chunk);
-}
-```
-
-
-
-### Discovering and Resuming Active Streams
-
-When your application starts or recovers from a crash, you can check for any active background streams and resume them. This is particularly useful for:
-- **Application restarts**: Resume processing after deployments or crashes
-- **Load balancing**: Pick up streams started by other instances
-- **Monitoring**: Check progress of long-running operations from different clients
-
-
-```curl curl maxLines=50
-# Step 1: Find active background streams for your agents
-curl --request GET \
- --url https://api.letta.com/v1/runs/active \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "agent_ids": [
- "agent-123",
- "agent-456"
- ],
- "background": true
-}'
-# Returns: [{"run_id": "run-abc", "agent_id": "agent-123", "status": "processing", ...}]
-
-# Step 2: Resume streaming from the beginning (or any specified seq_id)
-curl --request GET \
- --url https://api.letta.com/v1/runs/$RUN_ID/stream \
- --header 'Accept: text/event-stream' \
- --data '{
- "starting_after": 0, # Start from beginning
- "batch_size": 1000 # Fetch historical chunks in larger batches
-}'
-```
-```python python maxLines=50
-# Find and resume active background streams
-active_runs = client.runs.active(
- agent_ids=["agent-123", "agent-456"],
- background=True,
-)
-
-if active_runs:
- # Resume the first active stream from the beginning
- run = active_runs[0]
- print(f"Resuming stream for run {run.id}, status: {run.status}")
-
- stream = client.runs.stream(
- run_id=run.id,
- starting_after=0, # Start from beginning
- batch_size=1000 # Fetch historical chunks in larger batches
- )
-
- # Each historical chunk is streamed one at a time, followed by new chunks as they become available
- for chunk in stream:
- print(chunk)
-```
-```typescript TypeScript maxLines=50
-// Find and resume active background streams
-const activeRuns = await client.runs.active({
- agentIds: ["agent-123", "agent-456"],
- background: true,
-});
-
-if (activeRuns.length > 0) {
- // Resume the first active stream from the beginning
- const run = activeRuns[0];
- console.log(`Resuming stream for run ${run.id}, status: ${run.status}`);
-
- const stream = await client.runs.stream(run.id, {
- startingAfter: 0, // Start from beginning
- batchSize: 1000 // Fetch historical chunks in larger batches
- });
-
- // Each historical chunk is streamed one at a time, followed by new chunks as they become available
- for await (const chunk of stream) {
- console.log(chunk);
- }
-}
-```
-
-
-## Option 2: Async Operations with Polling
-
-
-**Best for:** Usecases where you don't need real-time token streaming.
-
-
-Ideal for batch processing, scheduled jobs, or when you don't need real-time updates. The [async SDK method](/api-reference/agents/messages/create-async) queues your request and returns immediately, letting you check results later:
-
-
-```curl curl maxLines=50
-# Start async operation (returns immediately with run ID)
-curl --request POST \
- --url https://api.letta.com/v1/agents/$AGENT_ID/messages/async \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "messages": [
- {
- "role": "user",
- "content": "Run comprehensive analysis on this dataset"
- }
- ]
-}'
-
-# Poll for results using the returned run ID
-curl --request GET \
- --url https://api.letta.com/v1/runs/$RUN_ID
-```
-```python python maxLines=50
-# Start async operation (returns immediately with run ID)
-run = client.agents.messages.create_async(
- agent_id=agent_state.id,
- messages=[
- {
- "role": "user",
- "content": "Run comprehensive analysis on this dataset"
- }
- ],
-)
-
-# Poll for completion
-import time
-while run.status != "completed":
- time.sleep(2)
- run = client.runs.retrieve(run_id=run.id)
-
-# Get the messages once complete
-messages = client.runs.messages.list(run_id=run.id)
-```
-```typescript TypeScript maxLines=50
-// Start async operation (returns immediately with run ID)
-const run = await client.agents.createAgentMessageAsync({
- agentId: agentState.id,
- requestBody: {
- messages: [
- {
- role: "user",
- content: "Run comprehensive analysis on this dataset"
- }
- ]
- }
-});
-
-// Poll for completion
-while (run.status !== "completed") {
- await new Promise(resolve => setTimeout(resolve, 2000));
- run = await client.runs.retrieveRun({ runId: run.id });
-}
-
-// Get the messages once complete
-const messages = await client.runs.listRunMessages({ runId: run.id });
-```
-
-
-## Option 3: Configure Streaming with Keepalive Pings and Longer Timeouts
-
-
-**Best for:** Usecases where you are already using the standard [streaming code](/guides/agents/streaming), but are experiencing issues with timeouts or disconnects (e.g. due to network interruptions or hanging tool executions).
-
-**Trade-off:** Not as reliable as background mode, and does not support resuming a disconnected stream/request.
-
-
-
-This approach assumes a persistent HTTP connection. We highly recommend using **background mode** (or async polling) for long-running jobs, especially when:
-- Your infrastructure uses aggressive proxy timeouts
-- You need to handle network interruptions gracefully
-- Operations might exceed 10 minutes
-
-
-For operations under 10 minutes that need real-time updates without the complexity of background processing. Configure keepalive pings and timeouts to maintain stable connections:
-
-
-```curl curl maxLines=50
-curl --request POST \
- --url https://api.letta.com/v1/agents/$AGENT_ID/messages/stream \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "messages": [
- {
- "role": "user",
- "content": "Execute this long-running analysis"
- }
- ],
- "include_pings": true
-}'
-```
-```python python
-# Configure client with extended timeout
-from letta_client import Letta
-import os
-
-client = Letta(
- token=os.getenv("LETTA_API_KEY")
-)
-
-# Enable pings to prevent timeout during long operations
-stream = client.agents.messages.create_stream(
- agent_id=agent_state.id,
- messages=[
- {
- "role": "user",
- "content": "Execute this long-running analysis"
- }
- ],
- include_pings=True, # Sends periodic keepalive messages
- request_options={"timeout_in_seconds": 600} # 10 min timeout
-)
-
-# Process the stream (pings will keep connection alive)
-for chunk in stream:
- if chunk.message_type == "ping":
- # Keepalive ping received, connection is still active
- continue
- print(chunk)
-```
-```typescript TypeScript maxLines=50
-// Configure client with extended timeout
-import { Letta } from '@letta/sdk';
-
-const client = new Letta({
- token: process.env.LETTA_API_KEY
-});
-
-// Enable pings to prevent timeout during long operations
-const stream = await client.agents.createAgentMessageStream({
- agentId: agentState.id,
- requestBody: {
- messages: [
- {
- role: "user",
- content: "Execute this long-running analysis"
- }
- ],
- includePings: true // Sends periodic keepalive messages
- }, {
- timeoutInSeconds: 600 // 10 minutes timeout in seconds
- }
-});
-
-// Process the stream (pings will keep connection alive)
-for await (const chunk of stream) {
- if (chunk.message_type === "ping") {
- // Keepalive ping received, connection is still active
- continue;
- }
- console.log(chunk);
-}
-```
-
-
-### Configuration Guidelines
-
-| Parameter | Purpose | When to Use |
-|-----------|---------|------------|
-| Timeout in seconds | Extends request timeout beyond 60s default | Set to 1.5x your expected max duration |
-| Include pings | Sends keepalive messages every ~30s | Enable for operations with long gaps between outputs |
diff --git a/fern/pages/agents/memory-rewrite-proposal.mdx b/fern/pages/agents/memory-rewrite-proposal.mdx
deleted file mode 100644
index fa2510a6..00000000
--- a/fern/pages/agents/memory-rewrite-proposal.mdx
+++ /dev/null
@@ -1,295 +0,0 @@
----
-title: Agent Memory
-subtitle: How Letta agents manage and evolve their memory
-slug: guides/agents/memory
----
-
-
-Want to dive deeper? Read our blog posts on [agent memory](https://www.letta.com/blog/agent-memory), [context engineering](https://www.letta.com/blog/guide-to-context-engineering), [memory blocks](https://www.letta.com/blog/memory-blocks), and [RAG vs agent memory](https://www.letta.com/blog/rag-vs-agent-memory).
-
-
-## What is agent memory?
-
-**Agent memory in Letta is about managing what information is visible in the agent's context window.**
-
-Unlike traditional LLMs that are stateless (forgetting everything between interactions), Letta agents maintain persistent, evolving memory by intelligently managing their context window over time.
-
-The key insight: **the context window is a scarce resource.** You can't fit an entire conversation history or knowledge base into it. Effective memory is about:
-- **What's in context right now** (immediately visible to the LLM)
-- **What's been moved to external storage** (retrievable when needed)
-- **Who decides what stays and what goes** (the agent itself)
-
-## The LLM Operating System
-
-Letta is built on the [MemGPT](https://arxiv.org/abs/2310.08560) paper, which introduced the concept of an "LLM Operating System" for memory management. Just like a computer OS manages different types of memory (registers, RAM, disk), Letta agents manage different tiers of information:
-
-```mermaid
-flowchart TB
- subgraph ContextWindow["⚡ CONTEXT WINDOW (What the LLM sees)"]
- direction TB
- System[System Prompt
Kernel context]
- Blocks[Memory Blocks
Agent-managed context]
- Messages[Recent Messages
Conversation buffer]
- end
-
- subgraph External["💾 EXTERNAL STORAGE (Retrieved on-demand)"]
- direction TB
- Recall[Recall Memory
Full conversation history]
- Archival[Archival Memory
Explicit facts & knowledge]
- Files[Data Sources
Documents & files]
- end
-
- Blocks -->|Agent edits| Blocks
- Messages -->|Overflow| Recall
- ContextWindow -.->|Agent searches| External
-```
-
-### Memory tiers explained
-
-| Tier | Size | Speed | Managed By | Purpose |
-|------|------|-------|------------|---------|
-| **System Prompt** | ~1-2K tokens | Instant | System | Agent instructions & behavior |
-| **Memory Blocks** | ~2-4K tokens total | Instant | **Agent** | Self-editing structured memory |
-| **Message Buffer** | Variable | Instant | System | Recent conversation flow |
-| **Recall Memory** | Unlimited | 1-2 sec | Agent via search | Past conversation history |
-| **Archival Memory** | Unlimited | 1-2 sec | Agent via search | Explicit facts & knowledge |
-| **Data Sources** | Unlimited | 1-2 sec | Agent via search | Uploaded documents |
-
-## Memory blocks: Units of abstraction
-
-**Memory blocks are discrete, structured sections of the context window that agents can read and edit.**
-
-Think of memory blocks as "variables" that persist across interactions:
-
-```python
-# Traditional approach: everything is ephemeral
-messages = [
- {"role": "user", "content": "I'm Sarah, I like Python"},
- {"role": "assistant", "content": "Hi Sarah!"},
- {"role": "user", "content": "What's my name?"}, # Model only "knows" from message history
-]
-
-# Letta approach: structured, persistent memory blocks
-memory_blocks = [
- {
- "label": "human",
- "value": "Name: Sarah\nPreferences: Python programming",
- "description": "Key details about the user"
- },
- {
- "label": "persona",
- "value": "I am a helpful coding assistant",
- "description": "My identity and behavior"
- }
-]
-# Agent can edit these blocks over time as it learns more
-```
-
-### Why memory blocks?
-
-**Memory blocks solve the fundamental challenge of context window management:**
-
-1. **Consistency**: Same information is visible across all interactions (not dependent on what fits in message buffer)
-2. **Editability**: Agents can update their understanding over time (not just accumulate)
-3. **Structure**: Organized sections instead of unstructured message history
-4. **Control**: Agents decide what's important enough to persist
-
-### Default memory blocks
-
-Letta agents typically start with two memory blocks:
-
-**Persona Block** - Who the agent is
-```
-My name is Sam. I am a friendly, professional assistant who helps users
-with programming questions. I prefer concise explanations with code examples.
-```
-
-**Human Block** - Who the user is
-```
-The user's name is Sarah. She is a Python developer working on AI applications.
-She prefers detailed technical explanations and appreciates best practices.
-```
-
-You can add custom blocks for any purpose:
-- **Project context**: Current task, goals, progress
-- **Organization info**: Company policies, shared knowledge
-- **Conversation state**: Multi-step workflow tracking
-
-## Agentic context engineering
-
-**The key innovation in Letta: agents manage their own memory using tools.**
-
-Instead of a fixed context window or simple retrieval, agents actively decide:
-- What to remember (write to memory blocks)
-- What to forget (remove outdated information)
-- What to search for (query external storage)
-- How to organize knowledge (restructure memory blocks)
-
-### Memory management tools
-
-Agents have access to these built-in tools:
-
-- `memory_insert` - Add new information to a memory block
-- `memory_replace` - Update or rewrite part of a memory block
-- `conversation_search` - Search past messages (recall memory)
-- `archival_memory_insert` - Store facts in long-term storage
-- `archival_memory_search` - Retrieve facts from long-term storage
-
-Example of an agent using memory tools:
-
-```
-User: "I'm working on a Next.js app now, not Django anymore"
-
-Agent thinks: "User has shifted tech stacks. I should update my memory."
-Agent calls: memory_replace(
- block_label="human",
- old_text="She is a Python developer working on Django apps",
- new_text="She is a full-stack developer currently working on Next.js apps"
-)
-Agent responds: "Got it! I've updated my notes that you're now working with Next.js."
-```
-
-## RAG vs Agent Memory
-
-**Traditional RAG (Retrieval-Augmented Generation):**
-- Retrieves semantically similar chunks
-- One-shot retrieval per interaction
-- Purely reactive (only searches when prompted)
-- No persistent understanding
-
-**Letta Agent Memory:**
-- Maintains structured, editable memory in context
-- Multi-step retrieval (can paginate, refine searches)
-- Proactive management (updates memory as it learns)
-- Persistent understanding that improves over time
-
-### When to use what
-
-Use **memory blocks** for:
-- Information that should be consistently visible
-- Knowledge that evolves (user preferences, project state)
-- Structured context (persona, relationships, goals)
-
-Use **external memory (RAG-style)** for:
-- Large corpora of documents
-- Historical conversation logs
-- Facts that rarely change
-- Information that's too large for context
-
-**Best practice**: Combine both. Memory blocks hold the "executive summary" while external storage holds the full details.
-
-## Sleep-time agents
-
-
-Sleep-time agents are an advanced feature for memory management. See [sleep-time agents guide](/guides/agents/sleep-time-agents) for details.
-
-
-Letta supports **sleep-time compute**: background agents that process and optimize memory while the main agent is idle. This enables:
-
-- **Lower latency**: Main agent doesn't spend time on memory management
-- **Better memory**: Dedicated agent can do deeper analysis and reorganization
-- **Consistent memory**: Sleep-time agent maintains memory quality over time
-
-Think of it like how humans process memories during sleep - consolidating experiences and strengthening important connections.
-
-## Memory best practices
-
-### 1. Start with clear, specific memory blocks
-
-```python
-# ❌ Vague
-{"label": "info", "value": "stuff about the user"}
-
-# ✅ Specific
-{"label": "user_preferences", "value": "Prefers: Python, VS Code, detailed explanations\nDislikes: Java, Eclipse"}
-```
-
-### 2. Write good descriptions
-
-The `description` field tells the agent **when and how** to use the block:
-
-```python
-# ❌ Vague description
-{
- "label": "project",
- "description": "Project info",
- "value": "Building a chatbot"
-}
-
-# ✅ Clear description
-{
- "label": "project_context",
- "description": "Current project goals, status, and blockers. Update as progress is made.",
- "value": "Building a customer support chatbot. Status: MVP complete. Next: Add knowledge base integration."
-}
-```
-
-### 3. Use read-only blocks for shared knowledge
-
-```python
-# Shared organizational knowledge that shouldn't change
-{
- "label": "company_policies",
- "description": "Company policies and guidelines for reference",
- "value": "Support hours: 9am-5pm PT. Escalation path: ...",
- "read_only": True # Agent can read but not edit
-}
-```
-
-### 4. Monitor memory block usage
-
-- Check if blocks are hitting size limits
-- Review if agents are actually using the blocks effectively
-- Adjust descriptions if agents misuse blocks
-
-## Memory in multi-agent systems
-
-Memory blocks enable powerful multi-agent patterns:
-
-### Shared memory
-
-Multiple agents can share the same memory block:
-
-```python
-# Create shared organizational knowledge
-org_block = client.blocks.create(
- label="organization",
- value="Mission: Help users build AI agents...",
- description="Shared organizational context"
-)
-
-# Both agents see the same block
-agent1 = client.agents.create(block_ids=[org_block.id], ...)
-agent2 = client.agents.create(block_ids=[org_block.id], ...)
-```
-
-### Cross-agent memory updates
-
-Agents can update each other's memory:
-
-```python
-# Supervisor agent updates worker agent's context
-supervisor_tool = """
-def update_worker_context(new_task_description: str):
- client.agents.blocks.modify(
- agent_id=worker_agent_id,
- block_label="current_task",
- value=new_task_description
- )
-"""
-```
-
-## Next steps
-
-- [Memory Blocks API](/guides/agents/memory-blocks) - Creating and managing memory blocks
-- [Context Engineering](/guides/agents/context-engineering) - Advanced memory management patterns
-- [Multi-Agent Shared Memory](/guides/agents/multi-agent-memory) - Coordinating memory across agents
-- [Sleep-Time Agents](/guides/agents/sleep-time-agents) - Background memory processing
-
-## Further reading
-
-- [Blog: Agent Memory](https://www.letta.com/blog/agent-memory)
-- [Blog: Guide to Context Engineering](https://www.letta.com/blog/guide-to-context-engineering)
-- [Blog: Memory Blocks](https://www.letta.com/blog/memory-blocks)
-- [Blog: RAG vs Agent Memory](https://www.letta.com/blog/rag-vs-agent-memory)
-- [MemGPT Research Paper](https://arxiv.org/abs/2310.08560)
diff --git a/fern/pages/agents/memory.mdx b/fern/pages/agents/memory.mdx
deleted file mode 100644
index 4ce7b108..00000000
--- a/fern/pages/agents/memory.mdx
+++ /dev/null
@@ -1,114 +0,0 @@
----
-title: Agent Memory
-subtitle: What is agent memory, and how does it work?
-slug: guides/agents/memory
----
-
-## What is agent memory?
-
-**Agent memory in Letta is about managing what information is in the agent's context window.**
-
-The context window is a scarce resource - you can't fit everything into it. Effective memory management is about deciding what stays in context (immediately visible) and what moves to external storage (retrieved when needed).
-
-Agent memory enables AI agents to maintain persistent state, learn from interactions, and develop long-term relationships with users. Unlike traditional chatbots that treat each conversation as isolated, agents with sophisticated memory systems can build understanding over time.
-
-## Types of Memory in Letta
-
-Letta agents have access to multiple memory systems:
-
-### Core Memory (In-Context)
-Memory blocks are structured sections of the agent's context window that persist across all interactions. They are always visible - no retrieval needed.
-
-**Memory blocks are Letta's core abstraction.** You can create blocks with any descriptive label - the agent learns how to use them autonomously. This enables everything from simple user preferences to sophisticated multi-agent coordination.
-
-[Learn more about memory blocks →](/guides/agents/memory-blocks)
-
-### External Memory (Out-of-Context)
-External memory provides unlimited storage for information that doesn't need to be always visible. Agents retrieve from external memory on-demand using search tools.
-
-Letta provides several built-in external memory systems:
-- **Conversation search** - Search past messages using full-text and semantic search
-- **Archival memory** - Agent-managed semantically searchable database for facts and knowledge
-- **Letta Filesystem** - File management system for documents and data ([learn more](/guides/agents/filesystem))
-
-Agents can also access any external data source through [MCP servers](/guides/mcp/overview) or [custom tools](/guides/agents/custom-tools) - databases, APIs, vector stores, or third-party services.
-
-## How Agents Manage Their Memory
-
-**What makes Letta unique is that agents don't just read from memory - they actively manage it.** Unlike traditional RAG systems that passively retrieve information, Letta agents use built-in tools to decide what to remember, update, and search for.
-
-When a user mentions they've switched from Python to TypeScript, the agent may choose to update its memory:
-
-
-```typescript TypeScript
-memory_replace(
- block_label: "human",
- old_text: "Prefers Python for development",
- new_text: "Currently using TypeScript for main project"
-)
-```
-```python Python
-memory_replace(
- block_label="human",
- old_text="Prefers Python for development",
- new_text="Currently using TypeScript for main project"
-)
-```
-
-
-Agents have three primary tools for editing memory blocks:
-- `memory_replace` - Search and replace for precise edits
-- `memory_insert` - Insert a line into a block
-- `memory_rethink` - Rewrite an entire block
-
-These tools can be attached or detached based on your use case. Not all agents need all tools (for example, some agents may not need `memory_rethink`), and memory tools can be removed entirely from an agent if needed.
-
-The agent decides what information is important enough to persist in its memory blocks, actively maintaining this information over time. This enables agents to build understanding through conversation rather than just retrieving relevant documents.
-
-## Memory Blocks vs RAG
-
-Traditional RAG retrieves semantically similar chunks on-demand. Letta's memory blocks are **persistent, structured context** that agents actively maintain.
-
-**Use memory blocks for:**
-- Information that should always be visible (user preferences, agent persona)
-- Knowledge that evolves over time (project status, learned preferences)
-
-**Use external memory (RAG-style) for:**
-- Large document collections
-- Historical conversation logs
-- Static reference material
-
-**Best practice:** Use both together. Memory blocks hold the "executive summary" while external storage holds the full details.
-
-## Research Background
-
-Letta is built by the creators of [MemGPT](https://arxiv.org/abs/2310.08560), a research paper that introduced the concept of an "LLM Operating System" for memory management. The base agent design in Letta is a MemGPT-style agent, which inherits core principles of self-editing memory, memory hierarchy, and intelligent context window management.
-
-## Next steps
-
-
-
- Learn how to implement and configure memory blocks in your agents
-
-
- Optimize memory performance and advanced memory management
-
-
- Use shared memory across multiple agents
-
-
- Read the research behind Letta's memory system
-
-
diff --git a/fern/pages/agents/memory_blocks.mdx b/fern/pages/agents/memory_blocks.mdx
deleted file mode 100644
index 05801f76..00000000
--- a/fern/pages/agents/memory_blocks.mdx
+++ /dev/null
@@ -1,407 +0,0 @@
----
-title: Memory Blocks
-subtitle: Understanding the building blocks of agent memory
-slug: guides/agents/memory-blocks
----
-
-
-Interested in learning more about the origin of memory blocks? Read our [blog post](https://www.letta.com/blog/memory-blocks).
-
-
-## What are memory blocks?
-
-Memory blocks are structured sections of the agent's context window that persist across all interactions. They are always visible - no retrieval needed.
-
-**Memory blocks are Letta's core abstraction.** Create a block with a descriptive label and the agent learns how to use it. This simple mechanism enables capabilities impossible with traditional context management.
-
-**Key properties:**
-- **Agent-managed** - Agents autonomously organize information based on block labels
-- **Flexible** - Use for any purpose: knowledge, guidelines, state tracking, scratchpad space
-- **Shareable** - Multiple agents can access the same block; update once, visible everywhere
-- **Always visible** - Blocks stay in context, never need retrieval
-
-**Examples:**
-- Store tool usage guidelines so agents avoid past mistakes
-- Maintain working memory in a scratchpad block
-- Mirror external state (user's current document) for real-time awareness
-- Share read-only policies across all agents from a central source
-- Coordinate multi-agent systems: parent agents watch subagent result blocks update in real-time
-- Enable emergent behavior: add `performance_tracking` or `emotional_state` and watch agents start using them
-
-Memory blocks aren't just storage - they're a coordination primitive that enables sophisticated agent behavior.
-
-## Memory block structure
-
-Memory blocks represent a section of an agent's context window. An agent may have multiple memory blocks, or none at all. A memory block consists of:
-* A `label`, which is a unique identifier for the block
-* A `description`, which describes the purpose of the block
-* A `value`, which is the contents/data of the block
-* A `limit`, which is the size limit (in characters) of the block
-
-## The importance of the `description` field
-
-When making memory blocks, it's crucial to provide a good `description` field that accurately describes what the block should be used for.
-The `description` is the main information used by the agent to determine how to read and write to that block. Without a good description, the agent may not understand how to use the block.
-
-Because `persona` and `human` are two popular block labels, Letta autogenerates default descriptions for these blocks if you don't provide them. If you provide a description for a memory block labelled `persona` or `human`, the default description will be overridden.
-
-For `persona`, a good default is:
-> The persona block: Stores details about your current persona, guiding how you behave and respond. This helps you to maintain consistency and personality in your interactions.
-
-For `human`, a good default is:
-> The human block: Stores key details about the person you are conversing with, allowing for more personalized and friend-like conversation.
-
-## Read-only blocks
-
-Memory blocks are read-write by default (so the agent can update the block using memory tools), but can be set to read-only by setting the `read_only` field to `true`. When a block is read-only, the agent cannot update the block.
-
-Read-only blocks are useful when you want to give an agent access to information (for example, a shared memory block about an organization), but you don't want the agent to be able to make potentially destructive changes to the block.
-
-## Creating an agent with memory blocks
-
-When you create an agent, you can specify memory blocks to also be created with the agent. For most chat applications, we recommend create a `human` block (to represent memories about the user) and a `persona` block (to represent the agent's persona).
-
-```typescript TypeScript maxLines=50
-// install letta-client with `npm install @letta-ai/letta-client`
-import { LettaClient } from '@letta-ai/letta-client'
-
-// create a client connected to Letta Cloud
-const client = new LettaClient({
- token: process.env.LETTA_API_KEY
-});
-
-// create an agent with two basic self-editing memory blocks
-const agentState = await client.agents.create({
- memoryBlocks: [
- {
- label: "human",
- value: "The human's name is Bob the Builder.",
- limit: 5000
- },
- {
- label: "persona",
- value: "My name is Sam, the all-knowing sentient AI.",
- limit: 5000
- }
- ],
- model: "openai/gpt-4o-mini"
-});
-```
-```python title="python" maxLines=50
-# install letta_client with `pip install letta-client`
-from letta_client import Letta
-import os
-
-# create a client connected to Letta Cloud
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# create an agent with two basic self-editing memory blocks
-agent_state = client.agents.create(
- memory_blocks=[
- {
- "label": "human",
- "value": "The human's name is Bob the Builder.",
- "limit": 5000
- },
- {
- "label": "persona",
- "value": "My name is Sam, the all-knowing sentient AI.",
- "limit": 5000
- }
- ],
- model="openai/gpt-4o-mini"
-)
-```
-
-When the agent is created, the corresponding blocks are also created and attached to the agent, so that the block value will be in the context window.
-
-## Creating and attaching memory blocks
-You can also directly create blocks and attach them to an agent. This can be useful if you want to create blocks that are shared between multiple agents. If multiple agents are attached to a block, they will all have the block data in their context windows (essentially providing shared memory).
-
-Below is an example of creating a block directory, and attaching the block to two agents by specifying the `block_ids` field.
-
-```typescript TypeScript maxLines=50
-// create a persisted block, which can be attached to agents
-const block = await client.blocks.create({
- label: "organization",
- description: "A block to store information about the organization",
- value: "Organization: Letta",
- limit: 4000,
-});
-
-// create an agent with both a shared block and its own blocks
-const sharedBlockAgent1 = await client.agents.create({
- name: "shared_block_agent1",
- memoryBlocks: [
- {
- label: "persona",
- value: "I am agent 1"
- },
- ],
- blockIds: [block.id],
- model: "openai/gpt-4o-mini"
-});
-
-// create another agent with the same shared block
-const sharedBlockAgent2 = await client.agents.create({
- name: "shared_block_agent2",
- memoryBlocks: [
- {
- label: "persona",
- value: "I am agent 2"
- },
- ],
- blockIds: [block.id],
- model: "openai/gpt-4o-mini"
-});
-```
-```python title="python" maxLines=50
-# create a persisted block, which can be attached to agents
-block = client.blocks.create(
- label="organization",
- description="A block to store information about the organization",
- value="Organization: Letta",
- limit=4000,
-)
-
-# create an agent with both a shared block and its own blocks
-shared_block_agent1 = client.agents.create(
- name="shared_block_agent1",
- memory_blocks=[
- {
- "label": "persona",
- "value": "I am agent 1"
- },
- ],
- block_ids=[block.id],
- model="openai/gpt-4o-mini"
-)
-
-# create another agent sharing the block
-shared_block_agent2 = client.agents.create(
- name="shared_block_agent2",
- memory_blocks=[
- {
- "label": "persona",
- "value": "I am agent 2"
- },
- ],
- block_ids=[block.id],
- model="openai/gpt-4o-mini"
-)
-```
-
-You can also attach blocks to existing agents:
-
-```typescript TypeScript
-await client.agents.blocks.attach(agent.id, block.id);
-```
-```python Python
-client.agents.blocks.attach(agent_id=agent.id, block_id=block.id)
-```
-
-You can see all agents attached to a block by using the `block_id` field in the [blocks retrieve](/api-reference/blocks/retrieve) endpoint.
-
-## Managing blocks
-
-### Retrieving a block
-You can retrieve the contents of a block by ID. This is useful when blocks store finalized reports, code outputs, or other data you want to extract for use outside the agent.
-
-
-```typescript TypeScript
-const block = await client.blocks.retrieve(block.id);
-console.log(block.value); // access the block's content
-```
-```python Python
-block = client.blocks.retrieve(block.id)
-print(block.value) # access the block's content
-```
-
-
-### Listing blocks
-You can list all blocks, optionally filtering by label or searching by label text. This is useful for finding blocks across your project.
-
-
-```typescript TypeScript
-// list all blocks
-const blocks = await client.blocks.list();
-
-// filter by label
-const humanBlocks = await client.blocks.list({
- label: "human"
-});
-
-// search by label text
-const searchResults = await client.blocks.list({
- labelSearch: "organization"
-});
-```
-```python Python
-# list all blocks
-blocks = client.blocks.list()
-
-# filter by label
-human_blocks = client.blocks.list(label="human")
-
-# search by label text
-search_results = client.blocks.list(label_search="organization")
-```
-
-
-### Modifying a block
-You can directly modify a block's content, limit, description, or other properties. This is particularly useful for:
-- External scripts that provide up-to-date information to agents (e.g., syncing a text file to a block)
-- Updating shared blocks that multiple agents reference
-- Programmatically managing block content outside of agent interactions
-
-
-```typescript TypeScript
-// update the block's value - completely replaces the content
-await client.blocks.modify(block.id, {
- value: "Updated organization information: Letta - Building agentic AI"
-});
-
-// update multiple properties
-await client.blocks.modify(block.id, {
- value: "New content",
- limit: 6000,
- description: "Updated description"
-});
-```
-```python Python
-# update the block's value - completely replaces the content
-client.blocks.modify(
- block.id,
- value="Updated organization information: Letta - Building agentic AI"
-)
-
-# update multiple properties
-client.blocks.modify(
- block.id,
- value="New content",
- limit=6000,
- description="Updated description"
-)
-```
-
-
-
-**Setting `value` completely replaces the entire block content** - it is not an append operation. If multiple processes (agents or external scripts) modify the same block concurrently, the last write wins and overwrites all earlier changes. To avoid data loss:
-- Set blocks to **read-only** if you don't want agents to modify them
-- Only modify blocks directly in controlled scenarios where overwriting is acceptable
-- Ensure your application logic accounts for full replacements, not merges
-
-
-### Deleting a block
-You can delete a block when it's no longer needed. Note that deleting a block will remove it from all agents that have it attached.
-
-
-```typescript TypeScript
-await client.blocks.delete(block.id);
-```
-```python Python
-client.blocks.delete(block_id=block.id)
-```
-
-
-### Inspecting block usage
-See which agents have a block attached:
-
-
-```typescript TypeScript
-// list all agents that use this block
-const agentsWithBlock = await client.blocks.agents.list(block.id);
-console.log(`Used by ${agentsWithBlock.length} agents:`);
-for (const agent of agentsWithBlock) {
- console.log(` - ${agent.name}`);
-}
-
-// with pagination
-const agentsPage = await client.blocks.agents.list(block.id, {
- limit: 10,
- order: "asc"
-});
-```
-```python Python
-# list all agents that use this block
-agents_with_block = client.blocks.agents.list(block_id=block.id)
-print(f"Used by {len(agents_with_block)} agents:")
-for agent in agents_with_block:
- print(f" - {agent.name}")
-
-# with pagination
-agents_page = client.blocks.agents.list(
- block_id=block.id,
- limit=10,
- order="asc"
-)
-```
-
-
-## Agent-scoped block operations
-
-### Listing an agent's blocks
-You can retrieve all blocks attached to a specific agent. This shows you the complete memory configuration for that agent.
-
-
-```typescript TypeScript
-const agentBlocks = await client.agents.blocks.list(agent.id);
-```
-```python Python
-agent_blocks = client.agents.blocks.list(agent_id=agent.id)
-```
-
-
-### Retrieving an agent's block by label
-Instead of using a block ID, you can retrieve a block from a specific agent using its label. This is useful when you want to inspect what the agent currently knows about a specific topic.
-
-
-```typescript TypeScript
-// get the agent's current knowledge about the human
-const humanBlock = await client.agents.blocks.retrieve(
- agent.id,
- "human"
-);
-console.log(humanBlock.value);
-```
-```python Python
-# get the agent's current knowledge about the human
-human_block = client.agents.blocks.retrieve(
- agent_id=agent.id,
- block_label="human"
-)
-print(human_block.value)
-```
-
-
-### Modifying an agent's block
-You can modify a block through the agent-scoped endpoint using the block's label. This is useful for updating agent-specific memory without needing to know the block ID.
-
-
-```typescript TypeScript
-// update the agent's human block
-await client.agents.blocks.modify(agent.id, "human", {
- value: "The human's name is Alice. She prefers Python over TypeScript."
-});
-```
-```python Python
-# update the agent's human block
-client.agents.blocks.modify(
- agent_id=agent.id,
- block_label="human",
- value="The human's name is Alice. She prefers Python over TypeScript."
-)
-```
-
-
-### Detaching blocks from agents
-You can detach a block from an agent's context window. This removes the block from the agent's memory without deleting the block itself.
-
-
-```typescript TypeScript
-await client.agents.blocks.detach(agent.id, block.id);
-```
-```python Python
-client.agents.blocks.detach(agent_id=agent.id, block_id=block.id)
-```
-
diff --git a/fern/pages/agents/message_types.mdx b/fern/pages/agents/message_types.mdx
deleted file mode 100644
index 2a6fa472..00000000
--- a/fern/pages/agents/message_types.mdx
+++ /dev/null
@@ -1,459 +0,0 @@
----
-title: Message Types
-subtitle: Understanding message types and working with agent message history
-slug: guides/agents/message-types
----
-
-When you interact with a Letta agent and retrieve its message history using `client.agents.messages.list()`, you'll receive various types of messages that represent different aspects of the agent's execution. This guide explains all message types and how to work with them.
-
-## Overview
-
-Letta uses a structured message system where each message has a specific `message_type` field that indicates its purpose. Messages are returned as instances of `LettaMessageUnion`, which is a discriminated union of all possible message types.
-
-## Message Type Categories
-
-### User and System Messages
-
-#### `user_message`
-Messages sent by the user or system events packaged as user input.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "user_message";
- content: string | Array;
- name?: string;
- otid?: string;
- sender_id?: string;
-}
-```
-
-**Special User Message Subtypes:**
-User messages can contain JSON with a `type` field indicating special message subtypes:
-
-- **`login`** - User login events
- ```json
- {
- "type": "login",
- "last_login": "Never (first login)",
- "time": "2025-10-03 12:34:56 PM PDT-0700"
- }
- ```
-
-- **`user_message`** - Standard user messages
- ```json
- {
- "type": "user_message",
- "message": "Hello, agent!",
- "time": "2025-10-03 12:34:56 PM PDT-0700"
- }
- ```
-
-- **`system_alert`** - System notifications and alerts
- ```json
- {
- "type": "system_alert",
- "message": "System notification text",
- "time": "2025-10-03 12:34:56 PM PDT-0700"
- }
- ```
-
-#### `system_message`
-Messages generated by the system, typically used for internal context.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "system_message";
- content: string;
- name?: string;
-}
-```
-
-**Note:** System messages are never streamed back in responses; they're only visible when paginating through message history.
-
-### Agent Reasoning and Responses
-
-#### `reasoning_message`
-Represents the agent's internal reasoning or "chain of thought."
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "reasoning_message";
- reasoning: string;
- source: "reasoner_model" | "non_reasoner_model";
- signature?: string;
-}
-```
-
-**Fields:**
-- `reasoning` - The agent's internal thought process
-- `source` - Whether this was generated by a model with native reasoning (like o1) or via prompting
-- `signature` - Optional cryptographic signature for reasoning verification (for models that support it)
-
-#### `hidden_reasoning_message`
-Represents reasoning that has been hidden from the response.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "hidden_reasoning_message";
- state: "redacted" | "omitted";
- hidden_reasoning?: string;
-}
-```
-
-**Fields:**
-- `state: "redacted"` - The provider redacted the reasoning content
-- `state: "omitted"` - The API chose not to include reasoning (e.g., for o1/o3 models)
-
-#### `assistant_message`
-The actual message content sent by the agent.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "assistant_message";
- content: string | Array;
- name?: string;
-}
-```
-
-### Tool Execution Messages
-
-#### `tool_call_message`
-A request from the agent to execute a tool.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "tool_call_message";
- tool_call: {
- name: string;
- arguments: string; // JSON string
- tool_call_id: string;
- };
-}
-```
-
-**Example:**
-```typescript
-{
- message_type: "tool_call_message",
- tool_call: {
- name: "archival_memory_search",
- arguments: '{"query": "user preferences", "page": 0}',
- tool_call_id: "call_abc123"
- }
-}
-```
-
-#### `tool_return_message`
-The result of a tool execution.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "tool_return_message";
- tool_return: string;
- status: "success" | "error";
- tool_call_id: string;
- stdout?: string[];
- stderr?: string[];
-}
-```
-
-**Fields:**
-- `tool_return` - The formatted return value from the tool
-- `status` - Whether the tool executed successfully
-- `stdout`/`stderr` - Captured output from the tool execution (useful for debugging)
-
-### Human-in-the-Loop Messages
-
-#### `approval_request_message`
-A request for human approval before executing a tool.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "approval_request_message";
- tool_call: {
- name: string;
- arguments: string;
- tool_call_id: string;
- };
-}
-```
-
-See [Human-in-the-Loop](/guides/agents/human-in-the-loop) for more information on this experimental feature.
-
-#### `approval_response_message`
-The user's response to an approval request.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "approval_response_message";
- approve: boolean;
- approval_request_id: string;
- reason?: string;
-}
-```
-
-## Working with Messages
-
-### Listing Messages
-
-
-```typescript TypeScript
-import { LettaClient } from "@letta-ai/letta-client";
-
-const client = new LettaClient({
- baseUrl: "https://api.letta.com",
-});
-
-// List recent messages
-const messages = await client.agents.messages.list("agent-id", {
- limit: 50,
- useAssistantMessage: true,
-});
-
-// Iterate through message types
-for (const message of messages) {
- switch (message.messageType) {
- case "user_message":
- console.log("User:", message.content);
- break;
- case "assistant_message":
- console.log("Agent:", message.content);
- break;
- case "reasoning_message":
- console.log("Reasoning:", message.reasoning);
- break;
- case "tool_call_message":
- console.log("Tool call:", message.toolCall.name);
- break;
- // ... handle other types
- }
-}
-```
-
-```python Python
-from letta_client import Letta
-
-client = Letta(base_url="https://api.letta.com")
-
-# List recent messages
-messages = client.agents.messages.list(
- agent_id="agent-id",
- limit=50,
- use_assistant_message=True
-)
-
-# Iterate through message types
-for message in messages:
- if message.message_type == "user_message":
- print(f"User: {message.content}")
- elif message.message_type == "assistant_message":
- print(f"Agent: {message.content}")
- elif message.message_type == "reasoning_message":
- print(f"Reasoning: {message.reasoning}")
- elif message.message_type == "tool_call_message":
- print(f"Tool call: {message.tool_call.name}")
- # ... handle other types
-```
-
-
-### Filtering Messages by Type
-
-
-```typescript TypeScript
-// Get only assistant messages (what the agent said to the user)
-const agentMessages = messages.filter(
- (msg) => msg.messageType === "assistant_message"
-);
-
-// Get all tool-related messages
-const toolMessages = messages.filter(
- (msg) => msg.messageType === "tool_call_message" ||
- msg.messageType === "tool_return_message"
-);
-
-// Get conversation history (user + assistant messages only)
-const conversation = messages.filter(
- (msg) => msg.messageType === "user_message" ||
- msg.messageType === "assistant_message"
-);
-```
-
-```python Python
-# Get only assistant messages (what the agent said to the user)
-agent_messages = [
- msg for msg in messages
- if msg.message_type == "assistant_message"
-]
-
-# Get all tool-related messages
-tool_messages = [
- msg for msg in messages
- if msg.message_type in ["tool_call_message", "tool_return_message"]
-]
-
-# Get conversation history (user + assistant messages only)
-conversation = [
- msg for msg in messages
- if msg.message_type in ["user_message", "assistant_message"]
-]
-```
-
-
-
-### Pagination
-
-Messages support cursor-based pagination:
-
-
-```typescript TypeScript
-// Get first page
-let messages = await client.agents.messages.list("agent-id", {
- limit: 100,
-});
-
-// Get next page using the last message ID
-const lastMessageId = messages[messages.length - 1].id;
-const nextPage = await client.agents.messages.list("agent-id", {
- limit: 100,
- before: lastMessageId,
-});
-```
-
-```python Python
-# Get first page
-messages = client.agents.messages.list(
- agent_id="agent-id",
- limit=100
-)
-
-# Get next page using the last message ID
-last_message_id = messages[-1].id
-next_page = client.agents.messages.list(
- agent_id="agent-id",
- limit=100,
- before=last_message_id
-)
-```
-
-
-## Message Metadata Fields
-
-All message types include these common fields:
-
-- **`id`** - Unique identifier for the message
-- **`date`** - ISO 8601 timestamp of when the message was created
-- **`message_type`** - The discriminator field identifying the message type
-- **`name`** - Optional name field (varies by message type)
-- **`otid`** - Offline threading ID for message correlation
-- **`sender_id`** - The ID of the sender (identity or agent ID)
-- **`step_id`** - The step ID associated with this message
-- **`is_err`** - Whether this message is part of an error step (debugging only)
-- **`seq_id`** - Sequence ID for ordering
-- **`run_id`** - The run ID associated with this message
-
-## Best Practices
-
-### 1. Use Type Discriminators
-
-Always check the `message_type` field to safely access type-specific fields:
-
-
-```typescript TypeScript
-if (message.messageType === "tool_call_message") {
- // TypeScript now knows message has a toolCall field
- console.log(message.toolCall.name);
-}
-```
-
-```python Python
-if message.message_type == "tool_call_message":
- # Safe to access tool_call
- print(message.tool_call.name)
-```
-
-
-### 2. Handle Special User Messages
-
-When displaying conversations to end users, filter out internal messages:
-
-```python
-def is_internal_message(msg):
- """Check if a user message is internal (login, system_alert, etc.)"""
- if msg.message_type != "user_message":
- return False
-
- if not isinstance(msg.content, str):
- return False
-
- try:
- parsed = json.loads(msg.content)
- return parsed.get("type") in ["login", "system_alert"]
- except:
- return False
-
-# Get user-facing messages only
-display_messages = [
- msg for msg in messages
- if not is_internal_message(msg)
-]
-```
-
-### 3. Track Tool Execution
-
-Match tool calls with their returns using `tool_call_id`:
-
-```python
-# Build a map of tool calls to their returns
-tool_calls = {
- msg.tool_call.tool_call_id: msg
- for msg in messages
- if msg.message_type == "tool_call_message"
-}
-
-tool_returns = {
- msg.tool_call_id: msg
- for msg in messages
- if msg.message_type == "tool_return_message"
-}
-
-# Find failed tool calls
-for call_id, call_msg in tool_calls.items():
- if call_id in tool_returns:
- return_msg = tool_returns[call_id]
- if return_msg.status == "error":
- print(f"Tool {call_msg.tool_call.name} failed:")
- print(f" {return_msg.tool_return}")
-```
-
-## See Also
-
-- [Human-in-the-Loop](/guides/agents/human-in-the-loop) - Using approval messages
-- [Streaming Responses](/guides/agents/streaming) - Receiving messages in real-time
-- [API Reference](/api-reference/agents/messages/list) - Full API documentation
diff --git a/fern/pages/agents/message_types.mdx.bak2 b/fern/pages/agents/message_types.mdx.bak2
deleted file mode 100644
index 5d0afa36..00000000
--- a/fern/pages/agents/message_types.mdx.bak2
+++ /dev/null
@@ -1,459 +0,0 @@
----
-title: Message Types
-subtitle: Understanding message types and working with agent message history
-slug: guides/agents/message-types
----
-
-When you interact with a Letta agent and retrieve its message history using `client.agents.messages.list()`, you'll receive various types of messages that represent different aspects of the agent's execution. This guide explains all message types and how to work with them.
-
-## Overview
-
-Letta uses a structured message system where each message has a specific `message_type` field that indicates its purpose. Messages are returned as instances of `LettaMessageUnion`, which is a discriminated union of all possible message types.
-
-## Message Type Categories
-
-### User and System Messages
-
-#### `user_message`
-Messages sent by the user or system events packaged as user input.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "user_message";
- content: string | Array;
- name?: string;
- otid?: string;
- sender_id?: string;
-}
-```
-
-**Special User Message Subtypes:**
-User messages can contain JSON with a `type` field indicating special message subtypes:
-
-- **`login`** - User login events
- ```json
- {
- "type": "login",
- "last_login": "Never (first login)",
- "time": "2025-10-03 12:34:56 PM PDT-0700"
- }
- ```
-
-- **`user_message`** - Standard user messages
- ```json
- {
- "type": "user_message",
- "message": "Hello, agent!",
- "time": "2025-10-03 12:34:56 PM PDT-0700"
- }
- ```
-
-- **`system_alert`** - System notifications and alerts
- ```json
- {
- "type": "system_alert",
- "message": "System notification text",
- "time": "2025-10-03 12:34:56 PM PDT-0700"
- }
- ```
-
-#### `system_message`
-Messages generated by the system, typically used for internal context.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "system_message";
- content: string;
- name?: string;
-}
-```
-
-**Note:** System messages are never streamed back in responses; they're only visible when paginating through message history.
-
-### Agent Reasoning and Responses
-
-#### `reasoning_message`
-Represents the agent's internal reasoning or "chain of thought."
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "reasoning_message";
- reasoning: string;
- source: "reasoner_model" | "non_reasoner_model";
- signature?: string;
-}
-```
-
-**Fields:**
-- `reasoning` - The agent's internal thought process
-- `source` - Whether this was generated by a model with native reasoning (like o1) or via prompting
-- `signature` - Optional cryptographic signature for reasoning verification (for models that support it)
-
-#### `hidden_reasoning_message`
-Represents reasoning that has been hidden from the response.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "hidden_reasoning_message";
- state: "redacted" | "omitted";
- hidden_reasoning?: string;
-}
-```
-
-**Fields:**
-- `state: "redacted"` - The provider redacted the reasoning content
-- `state: "omitted"` - The API chose not to include reasoning (e.g., for o1/o3 models)
-
-#### `assistant_message`
-The actual message content sent by the agent.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "assistant_message";
- content: string | Array;
- name?: string;
-}
-```
-
-### Tool Execution Messages
-
-#### `tool_call_message`
-A request from the agent to execute a tool.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "tool_call_message";
- tool_call: {
- name: string;
- arguments: string; // JSON string
- tool_call_id: string;
- };
-}
-```
-
-**Example:**
-```typescript
-{
- message_type: "tool_call_message",
- tool_call: {
- name: "archival_memory_search",
- arguments: '{"query": "user preferences", "page": 0}',
- tool_call_id: "call_abc123"
- }
-}
-```
-
-#### `tool_return_message`
-The result of a tool execution.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "tool_return_message";
- tool_return: string;
- status: "success" | "error";
- tool_call_id: string;
- stdout?: string[];
- stderr?: string[];
-}
-```
-
-**Fields:**
-- `tool_return` - The formatted return value from the tool
-- `status` - Whether the tool executed successfully
-- `stdout`/`stderr` - Captured output from the tool execution (useful for debugging)
-
-### Human-in-the-Loop Messages
-
-#### `approval_request_message`
-A request for human approval before executing a tool.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "approval_request_message";
- tool_call: {
- name: string;
- arguments: string;
- tool_call_id: string;
- };
-}
-```
-
-See [Human-in-the-Loop](/guides/agents/human_in_the_loop) for more information on this experimental feature.
-
-#### `approval_response_message`
-The user's response to an approval request.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "approval_response_message";
- approve: boolean;
- approval_request_id: string;
- reason?: string;
-}
-```
-
-## Working with Messages
-
-### Listing Messages
-
-
-```typescript TypeScript
-import { LettaClient } from "@letta-ai/letta-client";
-
-const client = new LettaClient({
- baseUrl: "https://api.letta.com",
-});
-
-// List recent messages
-const messages = await client.agents.messages.list("agent-id", {
- limit: 50,
- useAssistantMessage: true,
-});
-
-// Iterate through message types
-for (const message of messages) {
- switch (message.messageType) {
- case "user_message":
- console.log("User:", message.content);
- break;
- case "assistant_message":
- console.log("Agent:", message.content);
- break;
- case "reasoning_message":
- console.log("Reasoning:", message.reasoning);
- break;
- case "tool_call_message":
- console.log("Tool call:", message.toolCall.name);
- break;
- // ... handle other types
- }
-}
-```
-
-```python Python
-from letta_client import Letta
-
-client = Letta(base_url="https://api.letta.com")
-
-# List recent messages
-messages = client.agents.messages.list(
- agent_id="agent-id",
- limit=50,
- use_assistant_message=True
-)
-
-# Iterate through message types
-for message in messages:
- if message.message_type == "user_message":
- print(f"User: {message.content}")
- elif message.message_type == "assistant_message":
- print(f"Agent: {message.content}")
- elif message.message_type == "reasoning_message":
- print(f"Reasoning: {message.reasoning}")
- elif message.message_type == "tool_call_message":
- print(f"Tool call: {message.tool_call.name}")
- # ... handle other types
-```
-
-
-### Filtering Messages by Type
-
-
-```typescript TypeScript
-// Get only assistant messages (what the agent said to the user)
-const agentMessages = messages.filter(
- (msg) => msg.messageType === "assistant_message"
-);
-
-// Get all tool-related messages
-const toolMessages = messages.filter(
- (msg) => msg.messageType === "tool_call_message" ||
- msg.messageType === "tool_return_message"
-);
-
-// Get conversation history (user + assistant messages only)
-const conversation = messages.filter(
- (msg) => msg.messageType === "user_message" ||
- msg.messageType === "assistant_message"
-);
-```
-
-```python Python
-# Get only assistant messages (what the agent said to the user)
-agent_messages = [
- msg for msg in messages
- if msg.message_type == "assistant_message"
-]
-
-# Get all tool-related messages
-tool_messages = [
- msg for msg in messages
- if msg.message_type in ["tool_call_message", "tool_return_message"]
-]
-
-# Get conversation history (user + assistant messages only)
-conversation = [
- msg for msg in messages
- if msg.message_type in ["user_message", "assistant_message"]
-]
-```
-
-
-
-### Pagination
-
-Messages support cursor-based pagination:
-
-
-```typescript TypeScript
-// Get first page
-let messages = await client.agents.messages.list("agent-id", {
- limit: 100,
-});
-
-// Get next page using the last message ID
-const lastMessageId = messages[messages.length - 1].id;
-const nextPage = await client.agents.messages.list("agent-id", {
- limit: 100,
- before: lastMessageId,
-});
-```
-
-```python Python
-# Get first page
-messages = client.agents.messages.list(
- agent_id="agent-id",
- limit=100
-)
-
-# Get next page using the last message ID
-last_message_id = messages[-1].id
-next_page = client.agents.messages.list(
- agent_id="agent-id",
- limit=100,
- before=last_message_id
-)
-```
-
-
-## Message Metadata Fields
-
-All message types include these common fields:
-
-- **`id`** - Unique identifier for the message
-- **`date`** - ISO 8601 timestamp of when the message was created
-- **`message_type`** - The discriminator field identifying the message type
-- **`name`** - Optional name field (varies by message type)
-- **`otid`** - Offline threading ID for message correlation
-- **`sender_id`** - The ID of the sender (identity or agent ID)
-- **`step_id`** - The step ID associated with this message
-- **`is_err`** - Whether this message is part of an error step (debugging only)
-- **`seq_id`** - Sequence ID for ordering
-- **`run_id`** - The run ID associated with this message
-
-## Best Practices
-
-### 1. Use Type Discriminators
-
-Always check the `message_type` field to safely access type-specific fields:
-
-
-```typescript TypeScript
-if (message.messageType === "tool_call_message") {
- // TypeScript now knows message has a toolCall field
- console.log(message.toolCall.name);
-}
-```
-
-```python Python
-if message.message_type == "tool_call_message":
- # Safe to access tool_call
- print(message.tool_call.name)
-```
-
-
-### 2. Handle Special User Messages
-
-When displaying conversations to end users, filter out internal messages:
-
-```python
-def is_internal_message(msg):
- """Check if a user message is internal (heartbeat, login, etc.)"""
- if msg.message_type != "user_message":
- return False
-
- if not isinstance(msg.content, str):
- return False
-
- try:
- parsed = json.loads(msg.content)
- return parsed.get("type") in ["heartbeat", "login", "system_alert"]
- except:
- return False
-
-# Get user-facing messages only
-display_messages = [
- msg for msg in messages
- if not is_internal_message(msg)
-]
-```
-
-### 3. Track Tool Execution
-
-Match tool calls with their returns using `tool_call_id`:
-
-```python
-# Build a map of tool calls to their returns
-tool_calls = {
- msg.tool_call.tool_call_id: msg
- for msg in messages
- if msg.message_type == "tool_call_message"
-}
-
-tool_returns = {
- msg.tool_call_id: msg
- for msg in messages
- if msg.message_type == "tool_return_message"
-}
-
-# Find failed tool calls
-for call_id, call_msg in tool_calls.items():
- if call_id in tool_returns:
- return_msg = tool_returns[call_id]
- if return_msg.status == "error":
- print(f"Tool {call_msg.tool_call.name} failed:")
- print(f" {return_msg.tool_return}")
-```
-
-## See Also
-
-- [Human-in-the-Loop](/guides/agents/human_in_the_loop) - Using approval messages
-- [Streaming Responses](/guides/agents/streaming) - Receiving messages in real-time
-- [API Reference](/api-reference/agents/messages/list) - Full API documentation
diff --git a/fern/pages/agents/message_types.mdx.bak3 b/fern/pages/agents/message_types.mdx.bak3
deleted file mode 100644
index e338597b..00000000
--- a/fern/pages/agents/message_types.mdx.bak3
+++ /dev/null
@@ -1,459 +0,0 @@
----
-title: Message Types
-subtitle: Understanding message types and working with agent message history
-slug: guides/agents/message-types
----
-
-When you interact with a Letta agent and retrieve its message history using `client.agents.messages.list()`, you'll receive various types of messages that represent different aspects of the agent's execution. This guide explains all message types and how to work with them.
-
-## Overview
-
-Letta uses a structured message system where each message has a specific `message_type` field that indicates its purpose. Messages are returned as instances of `LettaMessageUnion`, which is a discriminated union of all possible message types.
-
-## Message Type Categories
-
-### User and System Messages
-
-#### `user_message`
-Messages sent by the user or system events packaged as user input.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "user_message";
- content: string | Array;
- name?: string;
- otid?: string;
- sender_id?: string;
-}
-```
-
-**Special User Message Subtypes:**
-User messages can contain JSON with a `type` field indicating special message subtypes:
-
-- **`login`** - User login events
- ```json
- {
- "type": "login",
- "last_login": "Never (first login)",
- "time": "2025-10-03 12:34:56 PM PDT-0700"
- }
- ```
-
-- **`user_message`** - Standard user messages
- ```json
- {
- "type": "user_message",
- "message": "Hello, agent!",
- "time": "2025-10-03 12:34:56 PM PDT-0700"
- }
- ```
-
-- **`system_alert`** - System notifications and alerts
- ```json
- {
- "type": "system_alert",
- "message": "System notification text",
- "time": "2025-10-03 12:34:56 PM PDT-0700"
- }
- ```
-
-#### `system_message`
-Messages generated by the system, typically used for internal context.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "system_message";
- content: string;
- name?: string;
-}
-```
-
-**Note:** System messages are never streamed back in responses; they're only visible when paginating through message history.
-
-### Agent Reasoning and Responses
-
-#### `reasoning_message`
-Represents the agent's internal reasoning or "chain of thought."
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "reasoning_message";
- reasoning: string;
- source: "reasoner_model" | "non_reasoner_model";
- signature?: string;
-}
-```
-
-**Fields:**
-- `reasoning` - The agent's internal thought process
-- `source` - Whether this was generated by a model with native reasoning (like o1) or via prompting
-- `signature` - Optional cryptographic signature for reasoning verification (for models that support it)
-
-#### `hidden_reasoning_message`
-Represents reasoning that has been hidden from the response.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "hidden_reasoning_message";
- state: "redacted" | "omitted";
- hidden_reasoning?: string;
-}
-```
-
-**Fields:**
-- `state: "redacted"` - The provider redacted the reasoning content
-- `state: "omitted"` - The API chose not to include reasoning (e.g., for o1/o3 models)
-
-#### `assistant_message`
-The actual message content sent by the agent.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "assistant_message";
- content: string | Array;
- name?: string;
-}
-```
-
-### Tool Execution Messages
-
-#### `tool_call_message`
-A request from the agent to execute a tool.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "tool_call_message";
- tool_call: {
- name: string;
- arguments: string; // JSON string
- tool_call_id: string;
- };
-}
-```
-
-**Example:**
-```typescript
-{
- message_type: "tool_call_message",
- tool_call: {
- name: "archival_memory_search",
- arguments: '{"query": "user preferences", "page": 0}',
- tool_call_id: "call_abc123"
- }
-}
-```
-
-#### `tool_return_message`
-The result of a tool execution.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "tool_return_message";
- tool_return: string;
- status: "success" | "error";
- tool_call_id: string;
- stdout?: string[];
- stderr?: string[];
-}
-```
-
-**Fields:**
-- `tool_return` - The formatted return value from the tool
-- `status` - Whether the tool executed successfully
-- `stdout`/`stderr` - Captured output from the tool execution (useful for debugging)
-
-### Human-in-the-Loop Messages
-
-#### `approval_request_message`
-A request for human approval before executing a tool.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "approval_request_message";
- tool_call: {
- name: string;
- arguments: string;
- tool_call_id: string;
- };
-}
-```
-
-See [Human-in-the-Loop](/guides/agents/human_in_the_loop) for more information on this experimental feature.
-
-#### `approval_response_message`
-The user's response to an approval request.
-
-**Structure:**
-```typescript
-{
- id: string;
- date: datetime;
- message_type: "approval_response_message";
- approve: boolean;
- approval_request_id: string;
- reason?: string;
-}
-```
-
-## Working with Messages
-
-### Listing Messages
-
-
-```typescript TypeScript
-import { LettaClient } from "@letta-ai/letta-client";
-
-const client = new LettaClient({
- baseUrl: "https://api.letta.com",
-});
-
-// List recent messages
-const messages = await client.agents.messages.list("agent-id", {
- limit: 50,
- useAssistantMessage: true,
-});
-
-// Iterate through message types
-for (const message of messages) {
- switch (message.messageType) {
- case "user_message":
- console.log("User:", message.content);
- break;
- case "assistant_message":
- console.log("Agent:", message.content);
- break;
- case "reasoning_message":
- console.log("Reasoning:", message.reasoning);
- break;
- case "tool_call_message":
- console.log("Tool call:", message.toolCall.name);
- break;
- // ... handle other types
- }
-}
-```
-
-```python Python
-from letta_client import Letta
-
-client = Letta(base_url="https://api.letta.com")
-
-# List recent messages
-messages = client.agents.messages.list(
- agent_id="agent-id",
- limit=50,
- use_assistant_message=True
-)
-
-# Iterate through message types
-for message in messages:
- if message.message_type == "user_message":
- print(f"User: {message.content}")
- elif message.message_type == "assistant_message":
- print(f"Agent: {message.content}")
- elif message.message_type == "reasoning_message":
- print(f"Reasoning: {message.reasoning}")
- elif message.message_type == "tool_call_message":
- print(f"Tool call: {message.tool_call.name}")
- # ... handle other types
-```
-
-
-### Filtering Messages by Type
-
-
-```typescript TypeScript
-// Get only assistant messages (what the agent said to the user)
-const agentMessages = messages.filter(
- (msg) => msg.messageType === "assistant_message"
-);
-
-// Get all tool-related messages
-const toolMessages = messages.filter(
- (msg) => msg.messageType === "tool_call_message" ||
- msg.messageType === "tool_return_message"
-);
-
-// Get conversation history (user + assistant messages only)
-const conversation = messages.filter(
- (msg) => msg.messageType === "user_message" ||
- msg.messageType === "assistant_message"
-);
-```
-
-```python Python
-# Get only assistant messages (what the agent said to the user)
-agent_messages = [
- msg for msg in messages
- if msg.message_type == "assistant_message"
-]
-
-# Get all tool-related messages
-tool_messages = [
- msg for msg in messages
- if msg.message_type in ["tool_call_message", "tool_return_message"]
-]
-
-# Get conversation history (user + assistant messages only)
-conversation = [
- msg for msg in messages
- if msg.message_type in ["user_message", "assistant_message"]
-]
-```
-
-
-
-### Pagination
-
-Messages support cursor-based pagination:
-
-
-```typescript TypeScript
-// Get first page
-let messages = await client.agents.messages.list("agent-id", {
- limit: 100,
-});
-
-// Get next page using the last message ID
-const lastMessageId = messages[messages.length - 1].id;
-const nextPage = await client.agents.messages.list("agent-id", {
- limit: 100,
- before: lastMessageId,
-});
-```
-
-```python Python
-# Get first page
-messages = client.agents.messages.list(
- agent_id="agent-id",
- limit=100
-)
-
-# Get next page using the last message ID
-last_message_id = messages[-1].id
-next_page = client.agents.messages.list(
- agent_id="agent-id",
- limit=100,
- before=last_message_id
-)
-```
-
-
-## Message Metadata Fields
-
-All message types include these common fields:
-
-- **`id`** - Unique identifier for the message
-- **`date`** - ISO 8601 timestamp of when the message was created
-- **`message_type`** - The discriminator field identifying the message type
-- **`name`** - Optional name field (varies by message type)
-- **`otid`** - Offline threading ID for message correlation
-- **`sender_id`** - The ID of the sender (identity or agent ID)
-- **`step_id`** - The step ID associated with this message
-- **`is_err`** - Whether this message is part of an error step (debugging only)
-- **`seq_id`** - Sequence ID for ordering
-- **`run_id`** - The run ID associated with this message
-
-## Best Practices
-
-### 1. Use Type Discriminators
-
-Always check the `message_type` field to safely access type-specific fields:
-
-
-```typescript TypeScript
-if (message.messageType === "tool_call_message") {
- // TypeScript now knows message has a toolCall field
- console.log(message.toolCall.name);
-}
-```
-
-```python Python
-if message.message_type == "tool_call_message":
- # Safe to access tool_call
- print(message.tool_call.name)
-```
-
-
-### 2. Handle Special User Messages
-
-When displaying conversations to end users, filter out internal messages:
-
-```python
-def is_internal_message(msg):
- """Check if a user message is internal (heartbeat, login, etc.)"""
- if msg.message_type != "user_message":
- return False
-
- if not isinstance(msg.content, str):
- return False
-
- try:
- parsed = json.loads(msg.content)
- return parsed.get("type") in ["login", "system_alert"]
- except:
- return False
-
-# Get user-facing messages only
-display_messages = [
- msg for msg in messages
- if not is_internal_message(msg)
-]
-```
-
-### 3. Track Tool Execution
-
-Match tool calls with their returns using `tool_call_id`:
-
-```python
-# Build a map of tool calls to their returns
-tool_calls = {
- msg.tool_call.tool_call_id: msg
- for msg in messages
- if msg.message_type == "tool_call_message"
-}
-
-tool_returns = {
- msg.tool_call_id: msg
- for msg in messages
- if msg.message_type == "tool_return_message"
-}
-
-# Find failed tool calls
-for call_id, call_msg in tool_calls.items():
- if call_id in tool_returns:
- return_msg = tool_returns[call_id]
- if return_msg.status == "error":
- print(f"Tool {call_msg.tool_call.name} failed:")
- print(f" {return_msg.tool_return}")
-```
-
-## See Also
-
-- [Human-in-the-Loop](/guides/agents/human_in_the_loop) - Using approval messages
-- [Streaming Responses](/guides/agents/streaming) - Receiving messages in real-time
-- [API Reference](/api-reference/agents/messages/list) - Full API documentation
diff --git a/fern/pages/agents/multiagent.mdx b/fern/pages/agents/multiagent.mdx
deleted file mode 100644
index 0060addd..00000000
--- a/fern/pages/agents/multiagent.mdx
+++ /dev/null
@@ -1,120 +0,0 @@
----
-title: Multi-Agent Systems
-slug: guides/agents/multi-agent
----
-
-
-All agents in Letta are *stateful* - so when you build a multi-agent system in Letta, each agent can run both independently and with others via cross-agent messaging tools! The choice is yours.
-
-
-Letta provides built-in tools for supporting cross-agent communication to build multi-agent systems.
-To enable multi-agent collaboration, you should create agents that have access to the [built-in cross-agent communication tools](#built-in-multi-agent-tools) - either by attaching the tools in the ADE, or via the API or Python/TypeScript SDK.
-
-Letta agents can also share state via [shared memory blocks](/guides/agents/multi-agent-shared-memory). Shared memory blocks allow agents to have shared memory (e.g. memory about an organization they are both a part of or a task they are both working on).
-
-## Built-in Multi-Agent Tools
-
-We recommend only attaching one of `send_message_to_agent_and_wait_for_reply` or `send_message_to_agent_async`, but not both.
-Attaching both tools can cause the agent to become confused and use the tool less reliably.
-
-
-Our built-in tools for multi-agent communication can be used to create both **synchronous** and **asynchronous** communication networks between agents on your Letta server.
-However, because all agents in Letta are addressible via a REST API, you can also make your own custom tools that use the [API for messaging agents](/api-reference/agents/messages/create) to design your own version of agent-to-agent communication.
-
-There are three built-in tools for cross-agent communication:
-* `send_message_to_agent_async` for asynchronous multi-agent messaging,
-* `send_message_to_agent_and_wait_for_reply` for synchronous multi-agent messaging,
-* and `send_message_to_agents_matching_all_tags` for a "supervisor-worker" pattern
-
-### Messaging another agent (async / no wait)
-
-```typescript TypeScript
-// The function signature for the async multi-agent messaging tool
-function sendMessageToAgentAsync(
- message: string,
- otherAgentId: string
-): string
-```
-```python Python
-# The function signature for the async multi-agent messaging tool
-def send_message_to_agent_async(
- message: str,
- other_agent_id: str,
-): -> str
-```
-
-```mermaid
-sequenceDiagram
- autonumber
- Agent 1->>Agent 2: "Hi Agent 2 are you there?"
- Agent 2-->>Agent 1: "Your message has been delivered."
- Note over Agent 2: Processes message: "New message from Agent 1: ..."
- Agent 2->>Agent 1: "Hi Agent 1, yes I'm here!"
- Agent 1-->>Agent 2: "Your message has been delivered."
-```
-
-The `send_message_to_agent_async` tool allows one agent to send a message to another agent.
-This tool is **asynchronous**: instead of waiting for a response from the target agent, the agent will return immediately after sending the message.
-The message that is sent to the target agent contains a "message receipt", indicating which agent sent the message, which allows the target agent to reply to the sender (assuming they also have access to the `send_message_to_agent_async` tool).
-
-### Messaging another agent (wait for reply)
-
-```typescript TypeScript
-// The function signature for the synchronous multi-agent messaging tool
-function sendMessageToAgentAndWaitForReply(
- message: string,
- otherAgentId: string
-): string
-```
-```python Python
-# The function signature for the synchronous multi-agent messaging tool
-def send_message_to_agent_and_wait_for_reply(
- message: str,
- other_agent_id: str,
-): -> str
-```
-
-```mermaid
-sequenceDiagram
- autonumber
- Agent 1->>Agent 2: "Hi Agent 2 are you there?"
- Note over Agent 2: Processes message: "New message from Agent 1: ..."
- Agent 2->>Agent 1: "Hi Agent 1, yes I'm here!"
-```
-
-The `send_message_to_agent_and_wait_for_reply` tool also allows one agent to send a message to another agent.
-However, this tool is **synchronous**: the agent will wait for a response from the target agent before returning.
-The response of the target agent is returned in the tool output - if the target agent does not respond, the tool will return default message indicating no response was received.
-
-### Messaging a group of agents (supervisor-worker pattern)
-
-```typescript TypeScript
-// The function signature for the group broadcast multi-agent messaging tool
-function sendMessageToAgentsMatchingAllTags(
- message: string,
- tags: string[]
-): string[]
-```
-```python Python
-# The function signature for the group broadcast multi-agent messaging tool
-def send_message_to_agents_matching_all_tags(
- message: str,
- tags: List[str],
-) -> List[str]:
-```
-
-```mermaid
-sequenceDiagram
- autonumber
- Supervisor->>Worker 1: "Let's start the task"
- Supervisor->>Worker 2: "Let's start the task"
- Supervisor->>Worker 3: "Let's start the task"
- Note over Worker 1,Worker 3: All workers process their tasks
- Worker 1->>Supervisor: "Here's my result!"
- Worker 2->>Supervisor: "This is what I have"
- Worker 3->>Supervisor: "I didn't do anything..."
-```
-
-The `send_message_to_agents_matching_all_tags` tool allows one agent to send a message a larger group of agents in a "supervisor-worker" pattern.
-For example, a supervisor agent can use this tool to send a message asking all workers in a group to begin a task.
-This tool is also **synchronous**, so the result of the tool call will be a list of the responses from each agent in the group.
diff --git a/fern/pages/agents/multimodal.mdx b/fern/pages/agents/multimodal.mdx
deleted file mode 100644
index ffadab61..00000000
--- a/fern/pages/agents/multimodal.mdx
+++ /dev/null
@@ -1,163 +0,0 @@
----
-title: "Multi-modal (image inputs)"
-subtitle: "Send images to your agents"
-slug: "multimodal"
----
-
-
-Multi-modal features require compatible language models. Ensure your agent is configured with a multi-modal capable model.
-
-
-Letta agents support image inputs, enabling richer conversations and more powerful agent capabilities.
-
-## Model Support
-
-Multi-modal capabilities depend on the underlying language model.
-You can check which models from the API providers support image inputs by checking their individual model pages:
-
-- **[OpenAI](https://platform.openai.com/docs/models)**: GPT-4.1, o1/3/4, GPT-4o
-- **[Anthropic](https://docs.anthropic.com/en/docs/about-claude/models/overview)**: Claude Opus 4, Claude Sonnet 4
-- **[Gemini](https://ai.google.dev/gemini-api/docs/models)**: Gemini 2.5 Pro, Gemini 2.5 Flash
-
-If the provider you're using doesn't support image inputs, your images will still appear in the context window, but as a text message telling the agent that an image exists.
-
-## ADE Support
-
-You can pass images to your agents by drag-and-dropping them into the chat window, or clicking the image icon to select a manual file upload.
-
-
-
-
-## Usage Examples (SDK)
-
-### Sending an Image via URL
-
-
-```typescript TypeScript maxLines=100
-import { LettaClient } from '@letta-ai/letta-client';
-
-const client = new LettaClient({ token: "LETTA_API_KEY" });
-
-const response = await client.agents.messages.create(
- agentState.id, {
- messages: [
- {
- role: "user",
- content: [
- {
- type: "text",
- text: "Describe this image."
- },
- {
- type: "image",
- source: {
- type: "url",
- url: "https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg",
- },
- }
- ],
- }
- ],
- }
-);
-```
-```python title="python" maxLines=100
-from letta_client import Letta
-
-client = Letta(token="LETTA_API_KEY")
-
-response = client.agents.messages.create(
- agent_id=agent_state.id,
- messages=[
- {
- "role": "user",
- "content": [
- {
- "type": "text",
- "text": "Describe this image."
- },
- {
- "type": "image",
- "source": {
- "type": "url",
- "url": "https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg",
- },
- }
- ],
- }
- ],
-)
-```
-
-
-### Sending an Image via Base64
-
-
-```typescript TypeScript maxLines=100
-import { LettaClient } from '@letta-ai/letta-client';
-
-const client = new LettaClient({ token: "LETTA_API_KEY" });
-
-const imageUrl = "https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg";
-const imageResponse = await fetch(imageUrl);
-const imageBuffer = await imageResponse.arrayBuffer();
-const imageData = Buffer.from(imageBuffer).toString('base64');
-
-const response = await client.agents.messages.create(
- agentState.id, {
- messages: [
- {
- role: "user",
- content: [
- {
- type: "text",
- text: "Describe this image."
- },
- {
- type: "image",
- source: {
- type: "base64",
- mediaType: "image/jpeg",
- data: imageData,
- },
- }
- ],
- }
- ],
- }
-);
-```
-```python title="python" maxLines=100
-import base64
-import httpx
-from letta_client import Letta
-
-client = Letta(token="LETTA_API_KEY")
-
-image_url = "https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg"
-image_data = base64.standard_b64encode(httpx.get(image_url).content).decode("utf-8")
-
-response = client.agents.messages.create(
- agent_id=agent_state.id,
- messages=[
- {
- "role": "user",
- "content": [
- {
- "type": "text",
- "text": "Describe this image."
- },
- {
- "type": "image",
- "source": {
- "type": "base64",
- "media_type": "image/jpeg",
- "data": image_data,
- },
- }
- ],
- }
- ],
-)
-```
-
diff --git a/fern/pages/agents/multiuser.mdx b/fern/pages/agents/multiuser.mdx
deleted file mode 100644
index 4e933c1f..00000000
--- a/fern/pages/agents/multiuser.mdx
+++ /dev/null
@@ -1,177 +0,0 @@
----
-title: User Identities
-slug: guides/agents/multi-user
----
-
-You may be building a multi-user application with Letta, in which each user is associated with a specific agent.
-In this scenario, you can use **Identities** to associate each agent with a user in your application.
-
-## Using Identities
-Let's assume that you have an application with multiple users that you're building on a [self-hosted Letta server](/guides/server/docker) or [Letta Cloud](/guides/cloud).
-Each user has a unique username, starting at `user_1`, and incrementing up as you add more users to the platform.
-
-To associate agents you create in Letta with your users, you can first create an **Identity** object with the user's unique ID as the `identifier_key` for your user, and then specify the **Identity** object ID when creating an agent.
-
-For example, with `user_1`, we would create a new Identity object with `identifier_key="user_1"` and then pass `identity.id` into our [create agent request](/api-reference/agents/create):
-
-```curl title="curl"
-curl -X POST https://app.letta.com/v1/identities/ \
- -H "Authorization: Bearer " \
- -H "Content-Type: application/json" \
- -d '{
- "identifier_key": "user_1",
- "name": "Caren",
- "identity_type": "user"
-}'
-{"id":"identity-634d3994-5d6c-46e9-b56b-56e34fe34ca0","identifier_key":"user_1","name":"Caren","identity_type":"user","project_id":null,"agent_ids":[],"organization_id":"org-00000000-0000-4000-8000-000000000000","properties":[]}
-curl -X POST https://app.letta.com/v1/agents/ \
- -H "Authorization: Bearer " \
- -H "Content-Type: application/json" \
- -d '{
- "memory_blocks": [],
- "llm": "anthropic/claude-3-5-sonnet-20241022",
- "context_window_limit": 200000,
- "embedding": "openai/text-embedding-3-small",
- "identity_ids": ["identity-634d3994-5d6c-46e9-b56b-56e34fe34ca0"]
-}'
-```
-```python title="python"
-# assumes that you already instantiated a client
-identity = client.identities.create(
- identifier_key="user_1",
- name="Caren",
- identity_type="user"
-)
-agent = client.agents.create(
- memory_blocks=[],
- model="anthropic/claude-3-5-sonnet-20241022",
- context_window_limit=200000,
- identity_ids=[identity.id]
-)
-```
-
-```typescript TypeScript
-// assumes that you already instantiated a client
-const identity = await client.identities.create({
- identifierKey: "user_1",
- name: "Caren",
- identityType: "user"
-})
-const agent = await client.agents.create({
- memoryBlocks: [],
- model: "anthropic/claude-3-5-sonnet-20241022",
- contextWindowLimit: 200000,
- identityIds: [identity.id]
-});
-```
-
-
-Then, if I wanted to search for agents associated with a specific user (e.g. called `user_id`), I could use the `identifier_keys` parameter in the [list agents request](/api-reference/agents/list):
-
-```curl title="curl"
-curl -X GET "https://app.letta.com/v1/agents/?identifier_keys=user_1" \
- -H "Accept: application/json"
-```
-```python title="python"
-# assumes that you already instantiated a client
-user_agents = client.agents.list(
- identifier_keys=["user_1"]
-)
-```
-```typescript TypeScript
-// assumes that you already instantiated a client
-await client.agents.list({
- identifierKeys: ["user_1"]
-});
-```
-
-
-You can also create an identity object and attach it to an existing agent. This can be useful if you want to enable multiple users to interact with a single agent:
-
-```curl title="curl"
-curl -X POST https://app.letta.com/v1/identities/ \
- -H "Authorization: Bearer " \
- -H "Content-Type: application/json" \
- -d '{
- "identifier_key": "user_1",
- "name": "Sarah",
- "identity_type": "user"
- "agent_ids": ["agent-00000000-0000-4000-8000-000000000000"]
-}'
-```
-```python title="python"
-# assumes that you already instantiated a client
-identity = client.identities.create({
- identifier_key="user_1",
- name="Sarah",
- identity_type="user"
- agent_ids=["agent-00000000-0000-4000-8000-000000000000"]
-})
-```
-```typescript TypeScript
-// assumes that you already instantiated a client
-const identity = await client.identities.create({
- identifierKey: "user_1",
- name: "Sarah",
- identityType: "user"
- agentIds: ["agent-00000000-0000-4000-8000-000000000000"]
-})
-```
-
-
-### Using Agent Tags to Identify Users
-It's also possible to utilize our agent tags feature to associate agents with specific users. To associate agents you create in Letta with your users, you can specify a tag when creating an agent, and set the tag to the user's unique ID.
-This example assumes that you have a self-hosted Letta server running on localhost (for example, by running [`docker run ...`](/guides/server/docker)).
-
-
-```typescript TypeScript
-import { LettaClient } from '@letta-ai/letta-client';
-
-// Connect to Letta Cloud
-const client = new LettaClient({token: process.env.LETTA_API_KEY});
-const userId = "my_uuid";
-
-// create an agent with the userId tag
-const agent = await client.agents.create({
- memoryBlocks: [],
- model: "anthropic/claude-3-5-sonnet-20241022",
- contextWindowLimit: 200000,
- tags: [userId]
-});
-console.log(`Created agent with id ${agent.id}, tags ${agent.tags}`);
-
-// list agents
-const userAgents = await client.agents.list({tags: [userId]});
-const agentIds = userAgents.map(agent => agent.id);
-console.log(`Found matching agents ${agentIds}`);
-```
-```python Python
-from letta_client import Letta
-
-# Connect to Letta Cloud
-import os
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-user_id = "my_uuid"
-
-# create an agent with the user_id tag
-agent = client.agents.create(
- memory_blocks=[],
- model="anthropic/claude-3-5-sonnet-20241022",
- context_window_limit=200000,
- tags=[user_id]
-)
-print(f"Created agent with id {agent.id}, tags {agent.tags}")
-
-# list agents
-user_agents = client.agents.list(tags=[user_id])
-agent_ids = [agent.id for agent in user_agents]
-print(f"Found matching agents {agent_ids}")
-```
-
-
-
-## Creating and Viewing Tags in the ADE
-You can also modify tags in the ADE.
-Simply click the **Advanced Settings** tab in the top-left of the ADE to view an agent's tags.
-You can create new tags by typing the tag name in the input field and hitting enter.
-
diff --git a/fern/pages/agents/overview.mdx b/fern/pages/agents/overview.mdx
deleted file mode 100644
index 4e1dcfdd..00000000
--- a/fern/pages/agents/overview.mdx
+++ /dev/null
@@ -1,277 +0,0 @@
----
-title: Building Stateful Agents with Letta
-slug: guides/agents/overview
----
-
-
-**New to Letta?** If you haven't already, read [Core Concepts](/core-concepts) to understand how Letta's stateful agents are fundamentally different from traditional LLM APIs.
-
-
-Letta agents can automatically manage long-term memory, load data from external sources, and call custom tools.
-Unlike in other frameworks, Letta agents are stateful, so they keep track of historical interactions and reserve part of their context to read and write memories which evolve over time.
-
-
-
-
-
-Letta manages a reasoning loop for agents. At each agent step (i.e. iteration of the loop), the state of the agent is checkpointed and persisted to the database.
-
-You can interact with agents from a REST API, the ADE, and TypeScript / Python SDKs.
-As long as they are connected to the same service, all of these interfaces can be used to interact with the same agents.
-
-
-If you're interested in learning more about stateful agents, read our [blog post](https://www.letta.com/blog/stateful-agents).
-
-
-## Agents vs Threads
-
-In Letta, you can think of an agent as a single entity that has a single message history which is treated as infinite.
-The sequence of interactions the agent has experienced through its existence make up the agent's state (or memory).
-
-One distinction between Letta and other agent frameworks is that Letta does not have the notion of message *threads* (or *sessions*).
-Instead, there are only *stateful agents*, which have a single perpetual thread (sequence of messages).
-
-The reason we use the term *agent* rather than *thread* is because Letta is based on the principle that **all agents interactions should be part of the persistent memory**, as opposed to building agent applications around ephemeral, short-lived interactions (like a thread or session).
-```mermaid
-%%{init: {'flowchart': {'rankDir': 'LR'}}}%%
-flowchart LR
- subgraph Traditional["Thread-Based Agents"]
- direction TB
- llm1[LLM] --> thread1["Thread 1
- --------
- Ephemeral
- Session"]
- llm1 --> thread2["Thread 2
- --------
- Ephemeral
- Session"]
- llm1 --> thread3["Thread 3
- --------
- Ephemeral
- Session"]
- end
-
- Traditional ~~~ Letta
-
- subgraph Letta["Letta Stateful Agents"]
- direction TB
- llm2[LLM] --> agent["Single Agent
- --------
- Persistent Memory"]
- agent --> db[(PostgreSQL)]
- db -->|"Learn & Update"| agent
- end
-
- class thread1,thread2,thread3 session
- class agent agent
-```
-
-If you would like to create common starting points for new conversation "threads", we recommending using [agent templates](/guides/templates/overview) to create new agents for each conversation, or directly copying agent state from an existing agent.
-
-For multi-users applications, we recommend creating an agent per-user, though you can also have multiple users message a single agent (but it will be a single shared message history).
-
-## Create an agent
-
-To start creating agents with Letta Cloud, [create an API key](https://app.letta.com/api-keys) and set it as `LETTA_API_KEY` in your environment. For self-hosted deployments, see our [self-hosting guide](/guides/selfhosting/overview).
-
-
-You can create a new agent via the REST API, Python SDK, or TypeScript SDK:
-
-```curl curl
-curl -X POST https://api.letta.com/v1/agents \
- -H "Authorization: Bearer $LETTA_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{
- "memory_blocks": [
- {
- "value": "The human'\''s name is Bob the Builder.",
- "label": "human"
- },
- {
- "value": "My name is Sam, the all-knowing sentient AI.",
- "label": "persona"
- }
- ],
- "model": "openai/gpt-4o-mini",
- "context_window_limit": 16000
-}'
-```
-```python title="python" maxLines=50
-# install letta_client with `pip install letta-client`
-from letta_client import Letta
-import os
-
-# create a client connected to Letta Cloud (uses api.letta.com by default)
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# create an agent with two basic self-editing memory blocks
-agent_state = client.agents.create(
- memory_blocks=[
- {
- "label": "human",
- "value": "The human's name is Bob the Builder."
- },
- {
- "label": "persona",
- "value": "My name is Sam, the all-knowing sentient AI."
- }
- ],
- model="openai/gpt-4o-mini",
- context_window_limit=16000
-)
-
-# the AgentState object contains all the information about the agent
-print(agent_state)
-```
-```typescript TypeScript maxLines=50
-// install letta-client with `npm install @letta-ai/letta-client`
-import { LettaClient } from '@letta-ai/letta-client'
-
-// create a client connected to Letta Cloud (uses api.letta.com by default)
-const client = new LettaClient({
- token: process.env.LETTA_API_KEY
-});
-
-// create an agent with two basic self-editing memory blocks
-const agentState = await client.agents.create({
- memoryBlocks: [
- {
- label: "human",
- value: "The human's name is Bob the Builder."
- },
- {
- label: "persona",
- value: "My name is Sam, the all-knowing sentient AI."
- }
- ],
- model: "openai/gpt-4o-mini",
- contextWindowLimit: 16000
-});
-
-// the AgentState object contains all the information about the agent
-console.log(agentState);
-```
-
-You can also create an agent without any code using the [Agent Development Environment (ADE)](/agent-development-environment).
-All Letta agents are stored in a database on the Letta server, so you can access the same agents from the ADE, the REST API, the Python SDK, and the TypeScript SDK.
-
-The response will include information about the agent, including its `id`:
-```json
-{
- "id": "agent-43f8e098-1021-4545-9395-446f788d7389",
- "name": "GracefulFirefly",
- ...
-}
-```
-
-Once an agent is created, you can message it:
-
-```curl curl
-curl --request POST \
- --url https://api.letta.com/v1/agents/$AGENT_ID/messages \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "messages": [
- {
- "role": "user",
- "content": "hows it going????"
- }
- ]
-}'
-```
-```python title="python" maxLines=50
-# send a message to the agent
-response = client.agents.messages.create(
- agent_id=agent_state.id,
- messages=[
- {
- "role": "user",
- "content": "hows it going????"
- }
- ]
-)
-
-# the response object contains the messages and usage statistics
-print(response)
-
-# if we want to print the usage stats
-print(response.usage)
-
-# if we want to print the messages
-for message in response.messages:
- print(message)
-```
-```typescript TypeScript maxLines=50
-// send a message to the agent
-const response = await client.agents.messages.create(
- agentState.id, {
- messages: [
- {
- role: "user",
- content: "hows it going????"
- }
- ]
- }
-);
-
-// the response object contains the messages and usage statistics
-console.log(response);
-
-// if we want to print the usage stats
-console.log(response.usage)
-
-// if we want to print the messages
-for (const message of response.messages) {
- console.log(message);
-}
-```
-
-
-### Message Types
-The `response` object contains the following attributes:
-* `usage`: The usage of the agent after the message was sent (the prompt tokens, completition tokens, and total tokens)
-* `message`: A list of `LettaMessage` objects, generated by the agent
-
-#### `LettaMessage`
-The `LettaMessage` object is a simplified version of the `Message` object stored in the database backend.
-Since a `Message` can include multiple events like a chain-of-thought and function calls, `LettaMessage` simplifies messages to have the following types:
-* `reasoning_message`: The inner monologue (chain-of-thought) of the agent
-* `tool_call_message`: An agent's tool (function) call
-* `tool_call_return`: The result of executing an agent's tool (function) call
-* `assistant_message`: An agent's response message (direct response in current architecture, or `send_message` tool call in legacy architectures)
-* `system_message`: A system message (for example, an alert about the user logging in)
-* `user_message`: A user message
-
-
-In current Letta agents, `assistant_message` represents the agent's direct response. In legacy architectures (`memgpt_agent`, `memgpt_v2_agent`), it wraps the `send_message` tool call.
-
-If you prefer to see the raw tool call format in legacy agents, you can set `use_assistant_message` to `false` in the request `config` (see the [endpoint documentation](/api-reference/agents/messages/create)).
-
-
-## Common agent operations
-For more in-depth guide on the full set of Letta agent operations, check out our [API reference](/api-reference/overview), our extended [Python SDK](https://github.com/letta-ai/letta/blob/main/examples/docs/example.py) and [TypeScript SDK](https://github.com/letta-ai/letta/blob/main/examples/docs/node/example.ts) examples, as well as our other [cookbooks](/cookbooks).
-
-If you're using a self-hosted Letta server, you should set the **base URL** (`base_url` in Python, `baseUrl` in TypeScript) to the Letta server's URL (e.g. `http://localhost:8283`) when you create your client. See an example [here](/api-reference/overview).
-
-If you're using a self-hosted server, you can omit the token if you're not using [password protection](/guides/server/docker#password-protection-advanced).
-If you are using password protection, set your **token** to the **password**.
-If you're using Letta Cloud, you should set the **token** to your **Letta Cloud API key**.
-
-### Retrieving an agent's state
-The agent's state is always persisted, so you can retrieve an agent's state by its ID.
-
-
-The result of the call is an `AgentState` object:
-
-
-### List agents
-Replace `agent_id` with your actual agent ID.
-
-
-The result of the call is a list of `AgentState` objects:
-
-
-### Delete an agent
-To delete an agent, you can use the `DELETE` endpoint with your `agent_id`:
-
diff --git a/fern/pages/agents/parallel_tool_calling.mdx b/fern/pages/agents/parallel_tool_calling.mdx
deleted file mode 100644
index b9de3501..00000000
--- a/fern/pages/agents/parallel_tool_calling.mdx
+++ /dev/null
@@ -1,102 +0,0 @@
----
-title: Parallel Tool Calling
-slug: guides/agents/parallel-tool-calling
----
-
-When an agent calls multiple tools, Letta can execute them concurrently instead of sequentially.
-
-Parallel tool calling has two configuration levels:
-- **Agent LLM config**: Controls whether the LLM can request multiple tool calls at once
-- **Individual tool settings**: Controls whether requested tools actually execute in parallel or sequentially
-
-## Model Support
-
-Parallel tool calling is supported for OpenAI and Anthropic models.
-
-## Enabling Parallel Tool Calling
-
-### Agent Configuration
-
-Set `parallel_tool_calls: true` in the agent's LLM config:
-
-
-```typescript TypeScript
-const agent = await client.agents.create({
- llm_config: {
- model: "anthropic/claude-sonnet-4-20250514",
- parallel_tool_calls: true
- }
-});
-```
-```python Python
-agent = client.agents.create(
- llm_config={
- "model": "anthropic/claude-sonnet-4-20250514",
- "parallel_tool_calls": True
- }
-)
-```
-
-
-### Tool Configuration
-
-Individual tools must opt-in to parallel execution:
-
-
-```typescript TypeScript
-await client.tools.update(toolId, {
- enable_parallel_execution: true
-});
-```
-```python Python
-client.tools.update(
- tool_id=tool_id,
- enable_parallel_execution=True
-)
-```
-
-
-By default, tools execute sequentially (`enable_parallel_execution=False`).
-
-
-Only enable parallel execution for tools safe to run concurrently. Tools that modify shared state or have ordering dependencies should remain sequential.
-
-
-## ADE Configuration
-
-### Agent Toggle
-
-1. Open **Settings** → **LLM Config**
-2. Enable **"Parallel tool calls"**
-
-### Tool Toggle
-
-1. Open the **Tools** panel
-2. Click a tool to open it
-3. Go to the **Settings** tab
-4. Enable **"Enable parallel execution"**
-
-## Execution Behavior
-
-When the agent calls multiple tools:
-- Sequential tools execute one-by-one
-- Parallel-enabled tools execute concurrently
-- Mixed: sequential tools complete first, then parallel tools execute together
-
-Example:
-```
-Agent calls:
- - search_web (parallel: true)
- - search_database (parallel: true)
- - send_message (parallel: false)
-
-Execution:
- 1. send_message executes
- 2. search_web AND search_database execute concurrently
-```
-
-## Limitations
-
-- Parallel execution is automatically disabled when [tool rules](/guides/agents/tool-rules) are configured
-- Only enable for tools safe to run concurrently (e.g., read-only operations)
-- Tools that modify shared state should remain sequential
diff --git a/fern/pages/agents/run_code.mdx b/fern/pages/agents/run_code.mdx
deleted file mode 100644
index c7bcd454..00000000
--- a/fern/pages/agents/run_code.mdx
+++ /dev/null
@@ -1,253 +0,0 @@
----
-title: Code Interpreter
-subtitle: Execute code in a secure sandbox with full network access
-slug: guides/agents/run-code
----
-
-The `run_code` tool enables Letta agents to execute code in a secure sandboxed environment. Useful for data analysis, calculations, API calls, and programmatic computation.
-
-
-On [Letta Cloud](/guides/cloud/overview), this tool works out of the box. For self-hosted deployments, you'll need to [configure an E2B API key](#self-hosted-setup).
-
-
-
-Each execution runs in a **fresh environment** - variables, files, and state do not persist between runs.
-
-
-## Quick Start
-
-
-```python Python
-from letta import Letta
-
-client = Letta(token="LETTA_API_KEY")
-
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["run_code"],
- memory_blocks=[{
- "label": "persona",
- "value": "I can run Python code for data analysis and API calls."
- }]
-)
-```
-
-```typescript TypeScript
-import { LettaClient } from '@letta-ai/letta-client';
-
-const client = new LettaClient({ token: "LETTA_API_KEY" });
-
-const agent = await client.agents.create({
- model: "openai/gpt-4o",
- tools: ["run_code"],
- memoryBlocks: [{
- label: "persona",
- value: "I can run Python code for data analysis and API calls."
- }]
-});
-```
-
-
-## Tool Parameters
-
-| Parameter | Type | Options | Description |
-|-----------|------|---------|-------------|
-| `code` | `str` | Required | The code to execute |
-| `language` | `str` | `python`, `js`, `ts`, `r`, `java` | Programming language |
-
-## Return Format
-
-```json
-{
- "results": ["Last expression value"],
- "logs": {
- "stdout": ["Print statements"],
- "stderr": ["Error output"]
- },
- "error": "Error details if execution failed"
-}
-```
-
-**Output types:**
-- `results[]`: Last expression value (Jupyter-style)
-- `logs.stdout`: Print statements and standard output
-- `logs.stderr`: Error messages
-- `error`: Present if execution failed
-
-## Supported Languages
-
-| Language | Key Limitations |
-|----------|-----------------|
-| **Python** | None - full ecosystem available |
-| **JavaScript** | No npm packages - built-in Node modules only |
-| **TypeScript** | No npm packages - built-in Node modules only |
-| **R** | No tidyverse - base R only |
-| **Java** | JShell-style execution - no traditional class definitions |
-
-### Python
-
-Full Python ecosystem with common packages pre-installed:
-
-- **Data**: numpy, pandas, scipy, scikit-learn
-- **Web**: requests, aiohttp, beautifulsoup4
-- **Utilities**: matplotlib, PyYAML, Pillow
-
-Check available packages:
-```python
-import pkg_resources
-print([d.project_name for d in pkg_resources.working_set])
-```
-
-### JavaScript & TypeScript
-
-No npm packages available - only built-in Node modules.
-
-```javascript
-// Works
-const fs = require('fs');
-const http = require('http');
-
-// Fails
-const axios = require('axios');
-```
-
-### R
-
-Base R only - no tidyverse packages.
-
-```r
-# Works
-mean(c(1, 2, 3))
-
-# Fails
-library(ggplot2)
-```
-
-### Java
-
-JShell-style execution - statement-level only.
-
-```java
-// Works
-System.out.println("Hello");
-int x = 42;
-
-// Fails
-public class Main {
- public static void main(String[] args) { }
-}
-```
-
-## Network Access
-
-The sandbox has full network access for HTTP requests, API calls, and DNS resolution.
-
-```python
-import requests
-
-response = requests.get('https://api.github.com/repos/letta-ai/letta')
-data = response.json()
-print(f"Stars: {data['stargazers_count']}")
-```
-
-## No State Persistence
-
-Variables, files, and state do not carry over between executions. Each `run_code` call is completely isolated.
-
-```python
-# First execution
-x = 42
-
-# Second execution (separate run_code call)
-print(x) # Error: NameError: name 'x' is not defined
-```
-
-**Implications:**
-- Must re-import libraries each time
-- Files written to disk are lost
-- Cannot build up state across executions
-
-## Self-Hosted Setup
-
-For self-hosted servers, configure an E2B API key. [E2B](https://e2b.dev) provides the sandbox infrastructure.
-
-
-```bash Docker
-docker run \
- -e E2B_API_KEY="your_e2b_api_key" \
- letta/letta:latest
-```
-
-```yaml Docker Compose
-services:
- letta:
- environment:
- - E2B_API_KEY=your_e2b_api_key
-```
-
-```python Per-Agent
-agent = client.agents.create(
- tools=["run_code"],
- tool_env_vars={
- "E2B_API_KEY": "your_e2b_api_key"
- }
-)
-```
-
-
-## Common Patterns
-
-### Data Analysis
-```python
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["run_code"],
- memory_blocks=[{
- "label": "persona",
- "value": "I use Python with pandas and numpy for data analysis."
- }]
-)
-```
-
-### API Integration
-```python
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["run_code", "web_search"],
- memory_blocks=[{
- "label": "persona",
- "value": "I fetch data from APIs using run_code and search docs with web_search."
- }]
-)
-```
-
-### Statistical Analysis
-```python
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["run_code"],
- memory_blocks=[{
- "label": "persona",
- "value": "I perform statistical analysis using scipy and numpy."
- }]
-)
-```
-
-## When to Use
-
-| Use Case | Tool | Why |
-|----------|------|-----|
-| Data analysis | `run_code` | Full Python data stack |
-| Math calculations | `run_code` | Programmatic computation |
-| Live API data | `run_code` | Network + processing |
-| Web scraping | `run_code` | requests + BeautifulSoup |
-| Simple search | `web_search` | Purpose-built |
-| Persistent data | Archival memory | State persistence |
-
-## Related Documentation
-
-- [Utilities Overview](/guides/agents/prebuilt-tools)
-- [Web Search](/guides/agents/web-search)
-- [Fetch Webpage](/guides/agents/fetch-webpage)
-- [Custom Tools](/guides/agents/custom-tools)
-- [Tool Variables](/guides/agents/tool-variables)
diff --git a/fern/pages/agents/scheduling.mdx b/fern/pages/agents/scheduling.mdx
deleted file mode 100644
index 74e174e2..00000000
--- a/fern/pages/agents/scheduling.mdx
+++ /dev/null
@@ -1,213 +0,0 @@
-# Scheduling
-
-**Scheduling** is a technique for triggering Letta agents at regular intervals.
-Many real-world applications require proactive behavior, such as checking emails every few hours or scraping news sites.
-Scheduling can support autonomous agents with the capability to manage ongoing processes.
-
-
-Native scheduling functionality is on the Letta Cloud roadmap. The approaches described in this guide are temporary solutions that work with both self-hosted and cloud deployments.
-
-
-## Common Use Cases
-
-When building autonomous agents with Letta, you often need to trigger them at regular intervals for tasks like:
-
-- **System Monitoring**: Health checks that adapt based on historical patterns
-- **Data Processing**: Intelligent ETL processes that handle edge cases contextually
-- **Memory Maintenance**: Agents that optimize their own knowledge base over time
-- **Proactive Notifications**: Context-aware alerts that consider user preferences and timing
-- **Continuous Learning**: Agents that regularly ingest new information and update their understanding
-
-This guide covers simple approaches to implement scheduled agent interactions.
-
-## Option 1: Simple Loop
-
-The most straightforward approach for development and testing:
-
-
-```typescript TypeScript
-import { LettaClient } from '@letta-ai/letta-client';
-
-const client = new LettaClient({ token: process.env.LETTA_API_KEY });
-const agentId = "your_agent_id";
-
-while (true) {
- const response = await client.agents.messages.create(agentId, {
- messages: [{
- role: "user",
- content: `Scheduled check at ${new Date()}`
- }]
- });
- console.log(`[${new Date()}] Agent responded`);
- await new Promise(resolve => setTimeout(resolve, 300000)); // 5 minutes
-}
-```
-
-```python title="python"
-import time
-import os
-from letta_client import Letta
-from datetime import datetime
-
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-agent_id = "your_agent_id"
-
-while True:
- response = client.agents.messages.create(
- agent_id=agent_id,
- messages=[{
- "role": "user",
- "content": f"Scheduled check at {datetime.now()}"
- }]
- )
- print(f"[{datetime.now()}] Agent responded")
- time.sleep(300) # 5 minutes
-```
-
-
-**Pros:** Simple, easy to debug
-**Cons:** Blocks terminal, stops if process dies
-
-## Option 2: System Cron Jobs
-
-For production deployments, use cron for reliability:
-
-
-```typescript TypeScript
-#!/usr/bin/env node
-import { LettaClient } from '@letta-ai/letta-client';
-
-async function sendMessage() {
- try {
- const client = new LettaClient({ token: process.env.LETTA_API_KEY });
- const response = await client.agents.messages.create("your_agent_id", {
- messages: [{
- role: "user",
- content: "Scheduled maintenance check"
- }]
- });
- console.log(`[${new Date()}] Success`);
- } catch (error) {
- console.error(`[${new Date()}] Error:`, error);
- }
-}
-
-sendMessage();
-```
-
-```python title="python"
-#!/usr/bin/env python3
-from letta_client import Letta
-from datetime import datetime
-
-try:
- import os
- client = Letta(token=os.getenv("LETTA_API_KEY"))
- response = client.agents.messages.create(
- agent_id="your_agent_id",
- messages=[{
- "role": "user",
- "content": "Scheduled maintenance check"
- }]
- )
- print(f"[{datetime.now()}] Success")
-except Exception as e:
- print(f"[{datetime.now()}] Error: {e}")
-```
-
-
-Add to crontab with `crontab -e`:
-```bash
-*/5 * * * * /usr/bin/python3 /path/to/send_message.py >> /var/log/letta_cron.log 2>&1
-# or for Node.js:
-*/5 * * * * /usr/bin/node /path/to/send_message.js >> /var/log/letta_cron.log 2>&1
-```
-
-**Pros:** System-managed, survives reboots
-**Cons:** Requires cron access
-
-## Best Practices
-
-1. **Error Handling**: Always wrap API calls in try-catch blocks
-2. **Logging**: Log both successes and failures for debugging
-3. **Environment Variables**: Store credentials securely
-4. **Rate Limiting**: Respect API limits and add backoff for failures
-
-## Example: Memory Maintenance Bot
-
-Complete example that performs periodic memory cleanup:
-
-
-```typescript TypeScript
-#!/usr/bin/env node
-import { LettaClient } from '@letta-ai/letta-client';
-
-async function runMaintenance() {
- try {
- const client = new LettaClient({ token: process.env.LETTA_API_KEY });
- const agentId = "your_agent_id";
-
- const response = await client.agents.messages.create(agentId, {
- messages: [{
- role: "user",
- content: "Please review your memory blocks for outdated information and clean up as needed."
- }]
- });
-
- // Print any assistant messages
- for (const message of response.messages) {
- if (message.messageType === "assistant_message") {
- console.log(`Agent response: ${message.content?.substring(0, 100)}...`);
- }
- }
-
- } catch (error) {
- console.error("Maintenance failed:", error);
- }
-}
-
-// Run if called directly
-if (import.meta.url === `file://${process.argv[1]}`) {
- runMaintenance();
-}
-```
-
-```python title="python"
-#!/usr/bin/env python3
-import logging
-from datetime import datetime
-from letta_client import Letta
-
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s'
-)
-
-def run_maintenance():
- try:
- import os
- client = Letta(token=os.getenv("LETTA_API_KEY"))
- agent_id = "your_agent_id"
-
- response = client.agents.messages.create(
- agent_id=agent_id,
- messages=[{
- "role": "user",
- "content": "Please review your memory blocks for outdated information and clean up as needed."
- }]
- )
-
- # Print any assistant messages
- for message in response.messages:
- if message.message_type == "assistant_message":
- logging.info(f"Agent response: {message.content[:100]}...")
-
- except Exception as e:
- logging.error(f"Maintenance failed: {e}")
-
-if __name__ == "__main__":
- run_maintenance()
-```
-
-
-Choose the scheduling method that best fits your deployment environment. For production systems, cron offers the best reliability, while simple loops are perfect for development and testing.
diff --git a/fern/pages/agents/tool_variables.mdx b/fern/pages/agents/tool_variables.mdx
deleted file mode 100644
index 31d68c6b..00000000
--- a/fern/pages/agents/tool_variables.mdx
+++ /dev/null
@@ -1,53 +0,0 @@
----
-title: Using Tool Variables
-slug: guides/agents/tool-variables
----
-
-You can use **tool variables** to specify environment variables available to your custom tools.
-For example, if you set a tool variable `PASSWORD` to `banana`, then write a custom function that prints `os.getenv('PASSWORD')` in the tool, the function will print `banana`.
-
-## Assigning tool variables in the ADE
-
-To assign tool variables in the Agent Development Environment (ADE), click on **Env Vars** to open the **Environment Variables** viewer:
-
-
-
-Once in the **Environment Variables** viewer, click **+** to add a new tool variable if one does not exist.
-
-
-
-## Assigning tool variables in the API / SDK
-
-You can also assign tool variables on agent creation in the API with the `tool_exec_environment_variables` parameter:
-
-```curl title="curl" {7-9}
-curl -X POST https://api.letta.com/v1/agents \
- -H "Authorization: Bearer $LETTA_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{
- "memory_blocks": [],
- "llm":"openai/gpt-4o-mini",
- "tool_exec_environment_variables": {
- "API_KEY": "your-api-key-here"
- }
-}'
-```
-```python title="python" {5-7}
-agent_state = client.agents.create(
- memory_blocks=[],
- model="openai/gpt-4o-mini",
- tool_exec_environment_variables={
- "API_KEY": "your-api-key-here"
- }
-)
-```
-```typescript TypeScript {5-7}
-const agentState = await client.agents.create({
- memoryBlocks: [],
- model: "openai/gpt-4o-mini",
- toolExecEnvironmentVariables: {
- "API_KEY": "your-api-key-here"
- }
-});
-```
-
diff --git a/fern/pages/agents/web_search.mdx b/fern/pages/agents/web_search.mdx
deleted file mode 100644
index 697138df..00000000
--- a/fern/pages/agents/web_search.mdx
+++ /dev/null
@@ -1,480 +0,0 @@
----
-title: Web Search
-subtitle: Search the internet in real-time with AI-powered search
-slug: guides/agents/web-search
----
-
-The `web_search` and `fetch_webpage` tools enables Letta agents to search the internet for current information, research, and general knowledge using [Exa](https://exa.ai)'s AI-powered search engine.
-
-
-On [Letta Cloud](/guides/cloud/overview), these tools work out of the box. For self-hosted deployments, you'll need to [configure an Exa API key](#self-hosted-setup).
-
-
-## Web Search
-
-### Adding Web Search to an Agent
-
-
-```python Python
-from letta import Letta
-
-client = Letta(token="LETTA_API_KEY")
-
-agent = client.agents.create(
- model="openai/gpt-4o",
- embedding="openai/text-embedding-3-small",
- tools=["web_search"],
- memory_blocks=[
- {
- "label": "persona",
- "value": "I'm a research assistant who uses web search to find current information and cite sources."
- }
- ]
-)
-```
-
-```typescript TypeScript
-import { LettaClient } from '@letta-ai/letta-client';
-
-const client = new LettaClient({ token: "LETTA_API_KEY" });
-
-const agent = await client.agents.create({
- model: "openai/gpt-4o",
- embedding: "openai/text-embedding-3-small",
- tools: ["web_search"],
- memoryBlocks: [
- {
- label: "persona",
- value: "I'm a research assistant who uses web search to find current information and cite sources."
- }
- ]
-});
-```
-
-
-### Usage Example
-
-```python
-response = client.agents.messages.create(
- agent_id=agent.id,
- messages=[
- {
- "role": "user",
- "content": "What are the latest developments in agent-based AI systems?"
- }
- ]
-)
-```
-
-Your agent can now choose to use `web_search` when it needs current information.
-
-## Self-Hosted Setup
-
-For self-hosted Letta servers, you'll need an Exa API key.
-
-### Get an API Key
-
-1. Sign up at [dashboard.exa.ai](https://dashboard.exa.ai/)
-2. Copy your API key
-3. See [Exa pricing](https://docs.exa.ai) for rate limits and costs
-
-### Configuration Options
-
-
-```bash Docker
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e OPENAI_API_KEY="your_openai_key" \
- -e EXA_API_KEY="your_exa_api_key" \
- letta/letta:latest
-```
-
-```yaml Docker Compose
-version: '3.8'
-services:
- letta:
- image: letta/letta:latest
- ports:
- - "8283:8283"
- environment:
- - OPENAI_API_KEY=your_openai_key
- - EXA_API_KEY=your_exa_api_key
- volumes:
- - ~/.letta/.persist/pgdata:/var/lib/postgresql/data
-```
-
-```python Per-Agent Configuration
-agent = client.agents.create(
- model="openai/gpt-4o",
- embedding="openai/text-embedding-3-small",
- tools=["web_search"],
- tool_env_vars={
- "EXA_API_KEY": "your_exa_api_key"
- }
-)
-```
-
-
-## Tool Parameters
-
-The `web_search` tool supports advanced filtering and search customization:
-
-| Parameter | Type | Default | Description |
-|-----------|------|---------|-------------|
-| `query` | `str` | Required | The search query to find relevant web content |
-| `num_results` | `int` | 10 | Number of results to return (1-100) |
-| `category` | `str` | None | Focus search on specific content types (see below) |
-| `include_text` | `bool` | False | Whether to retrieve full page content (usually overflows context) |
-| `include_domains` | `List[str]` | None | List of domains to include in search results |
-| `exclude_domains` | `List[str]` | None | List of domains to exclude from search results |
-| `start_published_date` | `str` | None | Only return content published after this date (ISO format) |
-| `end_published_date` | `str` | None | Only return content published before this date (ISO format) |
-| `user_location` | `str` | None | Two-letter country code for localized results (e.g., "US") |
-
-### Available Categories
-
-Use the `category` parameter to focus your search on specific content types:
-
-| Category | Best For | Example Query |
-|----------|----------|---------------|
-| `company` | Corporate information, company websites | "Tesla energy storage solutions" |
-| `research paper` | Academic papers, arXiv, research publications | "transformer architecture improvements 2025" |
-| `news` | News articles, current events | "latest AI policy developments" |
-| `pdf` | PDF documents, reports, whitepapers | "climate change impact assessment" |
-| `github` | GitHub repositories, open source projects | "python async web scraping libraries" |
-| `tweet` | Twitter/X posts, social media discussions | "reactions to new GPT release" |
-| `personal site` | Blogs, personal websites, portfolios | "machine learning tutorial blogs" |
-| `linkedin profile` | LinkedIn profiles, professional bios | "AI research engineers at Google" |
-| `financial report` | Earnings reports, financial statements | "Apple Q4 2024 earnings" |
-
-### Return Format
-
-The tool returns a JSON-encoded string containing:
-
-```json
-{
- "query": "search query",
- "results": [
- {
- "title": "Page title",
- "url": "https://example.com",
- "published_date": "2025-01-15",
- "author": "Author name",
- "highlights": ["Key excerpt 1", "Key excerpt 2"],
- "summary": "AI-generated summary of the content",
- "text": "Full page content (only if include_text=true)"
- }
- ]
-}
-```
-
-## Best Practices
-
-### 1. Guide When to Search
-
-Provide clear instructions to your agent about when web search is appropriate:
-
-```python
-memory_blocks=[
- {
- "label": "persona",
- "value": "I'm a helpful assistant. I use web_search for current events, recent news, and topics requiring up-to-date information. I cite my sources."
- }
-]
-```
-
-### 2. Combine with Archival Memory
-
-Use web search for external/current information, and archival memory for your organization's internal data:
-
-```python
-# Create agent with both web_search and archival memory tools
-agent = client.agents.create(
- model="openai/gpt-4o",
- embedding="openai/text-embedding-3-small",
- tools=["web_search", "archival_memory_search", "archival_memory_insert"],
- memory_blocks=[
- {
- "label": "persona",
- "value": "I use web_search for current events and external research. I use archival_memory_search for company-specific information and internal documents."
- }
- ]
-)
-```
-
-See the [Archival Memory documentation](/guides/agents/archival-memory) for more information.
-
-### 3. Craft Effective Search Queries
-
-Exa uses neural search that understands semantic meaning. Your agent will generally form good queries naturally, but you can improve results by guiding it to:
-
-- **Be descriptive and specific**: "Latest research on RLHF techniques for language models" is better than "RLHF research"
-- **Focus on topics, not keywords**: "How companies are deploying AI agents in customer service" works better than "AI agents customer service deployment"
-- **Use natural language**: The search engine understands conversational queries like "What are the environmental impacts of Bitcoin mining?"
-- **Specify time ranges when relevant**: Guide your agent to use date filters for time-sensitive queries
-
-Example instruction in memory:
-
-```python
-memory_blocks=[
- {
- "label": "search_strategy",
- "value": "When searching, I craft clear, descriptive queries that focus on topics rather than keywords. I use the category and date filters when appropriate to narrow results."
- }
-]
-```
-
-### 4. Manage Context Window
-
-By default, `include_text` is `False` to avoid context overflow. The tool returns highlights and AI-generated summaries instead, which are more concise:
-
-```python
-memory_blocks=[
- {
- "label": "search_guidelines",
- "value": "I avoid setting include_text=true unless specifically needed, as full text usually overflows the context window. Highlights and summaries are usually sufficient."
- }
-]
-```
-
-## Common Patterns
-
-### Research Assistant
-
-```python
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["web_search"],
- memory_blocks=[
- {
- "label": "persona",
- "value": "I'm a research assistant. I search for relevant information, synthesize findings from multiple sources, and provide citations."
- }
- ]
-)
-```
-
-### News Monitor
-
-```python
-agent = client.agents.create(
- model="openai/gpt-4o-mini",
- tools=["web_search"],
- memory_blocks=[
- {
- "label": "persona",
- "value": "I monitor news and provide briefings on AI industry developments."
- },
- {
- "label": "topics",
- "value": "Focus: AI/ML, agent systems, LLM advancements"
- }
- ]
-)
-```
-
-### Customer Support
-
-```python
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["web_search"],
- memory_blocks=[
- {
- "label": "persona",
- "value": "I help customers by checking documentation, service status pages, and community discussions for solutions."
- }
- ]
-)
-```
-
-## Troubleshooting
-
-### Agent Not Using Web Search
-
-Check:
-1. Tool is attached: `"web_search"` in agent's tools list
-2. Instructions are clear about when to search
-3. Model has good tool-calling capabilities (GPT-4, Claude 3+)
-
-```python
-# Verify tools
-agent = client.agents.retrieve(agent_id=agent.id)
-print([tool.name for tool in agent.tools])
-```
-
-### Missing EXA_API_KEY
-
-If you see errors about missing API keys on self-hosted deployments:
-
-```bash
-# Check if set
-echo $EXA_API_KEY
-
-# Set for session
-export EXA_API_KEY="your_exa_api_key"
-
-# Docker example
-docker run -e EXA_API_KEY="your_exa_api_key" letta/letta:latest
-```
-
-## When to Use Web Search
-
-| Use Case | Tool | Why |
-|----------|------|-----|
-| Current events, news | `web_search` | Real-time information |
-| External research | `web_search` | Broad internet access |
-| Internal documents | Archival memory | Fast, static data |
-| User preferences | Memory blocks | In-context, instant |
-| General knowledge | Pre-trained model | No search needed |
-
-## Fetch Webpage
-
-
-```python Python
-from letta import Letta
-
-client = Letta(token="LETTA_API_KEY")
-
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["fetch_webpage"],
- memory_blocks=[{
- "label": "persona",
- "value": "I can fetch and read webpages to answer questions about online content."
- }]
-)
-```
-
-```typescript TypeScript
-import { LettaClient } from '@letta-ai/letta-client';
-
-const client = new LettaClient({ token: "LETTA_API_KEY" });
-
-const agent = await client.agents.create({
- model: "openai/gpt-4o",
- tools: ["fetch_webpage"],
- memoryBlocks: [{
- label: "persona",
- value: "I can fetch and read webpages to answer questions about online content."
- }]
-});
-```
-
-
-## Tool Parameters
-
-| Parameter | Type | Description |
-|-----------|------|-------------|
-| `url` | `str` | The URL of the webpage to fetch |
-
-## Return Format
-
-The tool returns webpage content as text/markdown.
-
-**With Exa API (if configured):**
-```json
-{
- "title": "Page title",
- "published_date": "2025-01-15",
- "author": "Author name",
- "text": "Full page content in markdown"
-}
-```
-
-**Fallback (without Exa):**
-Returns markdown-formatted text extracted from the HTML.
-
-## How It Works
-
-The tool uses a multi-tier approach:
-
-1. **Exa API** (if `EXA_API_KEY` is configured): Uses Exa's content extraction
-2. **Trafilatura** (fallback): Open-source text extraction to markdown
-3. **Readability + html2text** (final fallback): HTML cleaning and conversion
-
-## Self-Hosted Setup
-
-For enhanced fetching on self-hosted servers, optionally configure an Exa API key. Without it, the tool still works using open-source extraction.
-
-### Optional: Configure Exa
-
-
-```bash Docker
-docker run \
- -e EXA_API_KEY="your_exa_api_key" \
- letta/letta:latest
-```
-
-```yaml Docker Compose
-services:
- letta:
- environment:
- - EXA_API_KEY=your_exa_api_key
-```
-
-```python Per-Agent
-agent = client.agents.create(
- tools=["fetch_webpage"],
- tool_env_vars={
- "EXA_API_KEY": "your_exa_api_key"
- }
-)
-```
-
-
-## Common Patterns
-
-### Documentation Reader
-```python
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["fetch_webpage", "web_search"],
- memory_blocks=[{
- "label": "persona",
- "value": "I search for documentation with web_search and read it with fetch_webpage."
- }]
-)
-```
-
-### Research Assistant
-```python
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["fetch_webpage", "archival_memory_insert"],
- memory_blocks=[{
- "label": "persona",
- "value": "I fetch articles and store key insights in archival memory for later reference."
- }]
-)
-```
-
-### Content Summarizer
-```python
-agent = client.agents.create(
- model="openai/gpt-4o",
- tools=["fetch_webpage"],
- memory_blocks=[{
- "label": "persona",
- "value": "I fetch webpages and provide summaries of their content."
- }]
-)
-```
-
-## When to Use
-
-| Use Case | Tool | Why |
-|----------|------|-----|
-| Read specific webpage | `fetch_webpage` | Direct URL access |
-| Find webpages to read | `web_search` | Discovery first |
-| Read + search in one | `web_search` with `include_text=true` | Combined operation |
-| Multiple pages | `fetch_webpage` | Iterate over URLs |
-
-## Related Documentation
-
-- [Utilities Overview](/guides/agents/prebuilt-tools)
-- [Web Search](/guides/agents/web-search)
-- [Run Code](/guides/agents/run-code)
-- [Custom Tools](/guides/agents/custom-tools)
-- [Tool Variables](/guides/agents/tool-variables)
diff --git a/fern/pages/api/sdk_migration_guide.mdx b/fern/pages/api/sdk_migration_guide.mdx
deleted file mode 100644
index 993320ff..00000000
--- a/fern/pages/api/sdk_migration_guide.mdx
+++ /dev/null
@@ -1,1519 +0,0 @@
----
-title: SDK v1.0 Migration Guide
-subtitle: Upgrading from v0.x to v1.0
-slug: api-reference/sdk-migration-guide
----
-
-
-This guide covers migrating from Letta SDK v0.x (e.g., `1.0.0-alpha.2`) to v1.0 (e.g., `1.0.0-alpha.10`+). For agent architecture migrations, see the [Architecture Migration Guide](/guides/legacy/migration_guide).
-
-
-
-**Letta Cloud Only (for now)**
-
-This SDK v1.0 migration guide applies **only to Letta Cloud**.
-
-The current self-hosted Letta release (v0.13.x) does **not** support the v1.0 SDK. If you are self-hosting Letta, continue using SDK v0.x for now.
-
-**Coming soon:** We will be releasing a new open-source version of Letta that includes SDK v1.0 support for self-hosted deployments.
-
-To use the v1.0 SDK today, you must connect to Letta Cloud at `https://api.letta.com`.
-
-
-## Overview
-
-SDK v1.0 introduces breaking changes to improve consistency and align with modern API design patterns:
-
-- **Naming convention**: All properties now use `snake_case` instead of `camelCase`
-- **Client initialization**: Simplified client constructor with renamed parameters
-- **Method names**: Several methods renamed for clarity
-- **Type imports**: Types moved to subpath exports for better organization
-- **Enums**: Replaced with string literal types
-- **Tool calls**: Changed from single object to array structure
-- **Pagination**: List methods now return page objects
-
-## Quick Reference
-
-### Package Update
-
-Update your package dependency:
-
-
-```json package.json
-{
- "dependencies": {
-- "@letta-ai/letta-client": "1.0.0-alpha.2"
-+ "@letta-ai/letta-client": "1.0.0-alpha.10"
- }
-}
-```
-```toml pyproject.toml
-[tool.poetry.dependencies]
--letta-client = "1.0.0a2"
-+letta-client = "1.0.0a10"
-```
-
-
-### Import Changes
-
-
-```typescript TypeScript
-// Old
-- import { LettaClient, Letta } from "@letta-ai/letta-client";
-
-// New
-+ import Letta from "@letta-ai/letta-client";
-+ import type {
-+ Block,
-+ CreateBlock,
-+ AgentType
-+ } from "@letta-ai/letta-client/resources/agents/agents";
-+ import type {
-+ LettaMessageUnion,
-+ ApprovalCreate
-+ } from "@letta-ai/letta-client/resources/agents/messages";
-+ import type {
-+ LlmConfig
-+ } from "@letta-ai/letta-client/resources/models/models";
-```
-```python Python
-# Old
-- from letta_client import Letta, LettaClient
-
-# New
-+ from letta import Letta
-+ from letta.schemas.agent import Block, CreateBlock, AgentType
-+ from letta.schemas.message import LettaMessageUnion, ApprovalCreate
-+ from letta.schemas.llm_config import LlmConfig
-```
-
-
-### Client Instantiation
-
-
-```typescript TypeScript
-// Old
-- const client = new LettaClient({
-- token: process.env.LETTA_API_KEY,
-- baseUrl: "https://api.letta.com"
-- });
-
-// New
-+ const client = new Letta({
-+ apiKey: process.env.LETTA_API_KEY,
-+ baseURL: "https://api.letta.com"
-+ });
-```
-```python Python
-# Old
-- client = LettaClient(
-- token=os.environ["LETTA_API_KEY"],
-- base_url="https://api.letta.com"
-- )
-
-# New
-+ client = Letta(
-+ api_key=os.environ["LETTA_API_KEY"],
-+ base_url="https://api.letta.com"
-+ )
-```
-
-
-## Breaking Changes by Category
-
-### 1. Pagination
-
-All list endpoints now use cursor-based pagination with consistent parameters:
-
-
-```typescript TypeScript
-// Old - various pagination styles
-const messages = await client.agents.messages.list(agentId, {
- sort_by: "created_at",
- ascending: true
-});
-
-// New - standardized cursor pagination
-const messagesPage = await client.agents.messages.list(agentId, {
- before: "msg_123", // cursor (message ID)
- after: "msg_456", // cursor (message ID)
- limit: 50,
- order: "asc" // or "desc"
-});
-const messages = messagesPage.items;
-```
-```python Python
-# Old
-messages = client.agents.messages.list(
- agent_id=agent_id,
- sort_by="created_at",
- ascending=True
-)
-
-# New
-messages_page = client.agents.messages.list(
- agent_id=agent_id,
- before="msg_123",
- after="msg_456",
- limit=50,
- order="asc"
-)
-messages = messages_page.items
-```
-
-
-**Affected endpoints:**
-- `agents.list()` - renamed `sort_by` → `order_by`, `ascending` → `order`
-- `agents.messages.list()`
-- `agents.tools.list()`
-- `agents.blocks.list()`
-- `agents.files.list()`
-- `agents.folders.list()`
-- `agents.groups.list()`
-- `blocks.list()`
-- `folders.list()`
-- `folders.files.list()`
-- `folders.passages.list()`
-- `folders.agents.list()`
-- `groups.list()`
-- `groups.messages.list()`
-- `identities.list()`
-- `providers.list()`
-- `runs.list()`
-- `runs.messages.list()`
-- `runs.steps.list()`
-- `jobs.list()`
-- `steps.list()`
-- `tags.list()`
-- `tools.list()`
-- `batches.list()`
-- `batches.messages.list()`
-
-### 2. Method Renames and Endpoint Restructuring
-
-Many methods were reorganized for better SDK structure:
-
-
-```typescript TypeScript
-// Agent updates
-- await client.agents.modify(agentId, updates)
-+ await client.agents.update(agentId, updates)
-
-// Message operations
-- await client.agents.summarize_agent_conversation(agentId)
-+ await client.agents.messages.summarize(agentId)
-
-- await client.agents.cancel_agent_run(agentId)
-+ await client.agents.messages.cancel(agentId)
-
-- await client.agents.messages.preview_raw_payload(agentId, messages)
-+ await client.agents.messages.preview(agentId, messages)
-
-// Agent file operations
-- await client.agents.list_agent_files(agentId)
-+ await client.agents.files.list(agentId)
-
-// Export/Import
-- await client.agents.export_agent_serialized(agentId)
-+ await client.agents.export(agentId)
-
-- await client.agents.import_agent_serialized(file)
-+ await client.agents.import(file)
-
-// Folder operations
-- await client.folders.get_agents_for_folder(folderId)
-+ await client.folders.agents.list(folderId)
-
-- await client.folders.retrieve_folder_metadata(folderId)
-+ await client.folders.retrieve_metadata(folderId)
-
-// Provider operations
-- await client.providers.check_provider(providerId)
-+ await client.providers.check(providerId)
-
-// Telemetry
-- await client.telemetry.retrieve_provider_trace(stepId)
-+ await client.steps.trace(stepId)
-
-// Step metrics
-- await client.steps.retrieve_step_metrics(stepId)
-+ await client.steps.metrics.retrieve(stepId)
-
-// Batch messages
-- await client.messages.list_batch_messages(batchId)
-+ await client.batches.messages.list(batchId)
-
-// Multi-agent groups
-- agent.multi_agent_group
-+ agent.managed_group
-```
-```python Python
-# Agent updates
-- client.agents.modify(agent_id, **updates)
-+ client.agents.update(agent_id, **updates)
-
-// Message operations
-- client.agents.summarize_agent_conversation(agent_id)
-+ client.agents.messages.summarize(agent_id)
-
-- client.agents.cancel_agent_run(agent_id)
-+ client.agents.messages.cancel(agent_id)
-
-// Export/Import
-- client.agents.export_agent_serialized(agent_id)
-+ client.agents.export(agent_id)
-
-- client.agents.import_agent_serialized(file)
-+ client.agents.import(file)
-
-// Folder operations
-- client.folders.get_agents_for_folder(folder_id)
-+ client.folders.agents.list(folder_id)
-
-// Provider operations
-- client.providers.check_provider(provider_id)
-+ client.providers.check(provider_id)
-```
-
-
-### 3. Deprecations
-
-Several endpoints and fields are now deprecated:
-
-**Deprecated endpoints:**
-- `client.agents.search()` - use `client.agents.list()` with filters
-- `client.messages.search()` - use `client.agents.messages.list()` with filters
-- `client.runs.list_active()` - use `client.runs.list(active=True)`
-- `client.jobs.list_active()` - use `client.jobs.list(active=True)`
-- `client.folders.get_by_name()` - use `client.folders.list(name="...")`
-- **MCP routes under `/tools/mcp/servers`** - replaced with new `/mcp-servers` endpoints
- - All old MCP methods moved from `client.tools.mcp.servers` to `client.mcp_servers`
- - Now use server IDs and tool IDs instead of names
-- Sources-related routes - replaced with folders
-- Passages routes - replaced with archives
-- Legacy agent architecture routes
-- All `/count` endpoints
-
-**Deprecated fields:**
-- `agent.memory` - use `agent.blocks`
-- `step.messages` - use `client.steps.messages.list(step_id)`
-- `agent.identity_ids` - replaced with `agent.identities` (full objects)
-- `agent.multi_agent_group` - renamed to `agent.managed_group`
-- `use_assistant_message` parameter - no longer needed
-- `tool_exec_environment_variables` - renamed to `secrets`
-
-**Deprecated on agent/block objects:**
-- Template-related fields: `is_template`, `base_template_id`, `deployment_id`
-- `entity_id`, `preserve_on_migration`, `hidden`
-- `name` on blocks (use `label`)
-
-### 4. Property Names (camelCase → snake_case)
-
-All API properties now use `snake_case`:
-
-
-```typescript TypeScript
-// Agent properties
-- agent.llmConfig
-+ agent.llm_config
-
-- agent.contextWindowLimit
-+ agent.context_window_limit
-
-- agent.blockIds
-+ agent.block_ids
-
-- agent.includeBaseTools
-+ agent.include_base_tools
-
-- agent.includeBaseToolRules
-+ agent.include_base_tool_rules
-
-- agent.initialMessageSequence
-+ agent.initial_message_sequence
-
-// Message properties
-- message.messageType
-+ message.message_type
-
-- message.toolCallId
-+ message.tool_call_id
-
-- message.toolReturn
-+ message.tool_return
-
-- message.toolCall
-+ message.tool_calls // Also changed to array!
-
-// API parameters
-- streamTokens: true
-+ stream_tokens: true
-
-- approvalRequestId: id
-+ approval_request_id: id
-```
-```python Python
-# Agent properties
-- agent.llm_config # Already snake_case
-+ agent.llm_config # No change needed
-
-- agent.context_window_limit
-+ agent.context_window_limit # No change needed
-
-# Python SDK was already using snake_case
-# Most changes affect TypeScript/JavaScript only
-```
-
-
-### 2. Agent Type Specification
-
-
-```typescript TypeScript
-// Old
-- agentType: Letta.AgentType.LettaV1Agent
-
-// New
-+ agent_type: "letta_v1_agent" as AgentType
-```
-```python Python
-# Old
-- agent_type=AgentType.LETTA_V1_AGENT
-
-# New
-+ agent_type="letta_v1_agent"
-```
-
-
-### 3. Method Renames
-
-
-```typescript TypeScript
-// Agent updates
-- await client.agents.modify(agentId, { model, llmConfig })
-+ await client.agents.update(agentId, { model, llm_config })
-
-// Message streaming
-- client.agents.messages.createStream(agentId, { messages, streamTokens })
-+ client.agents.messages.stream(agentId, { messages, stream_tokens })
-```
-```python Python
-# Agent updates
-- client.agents.modify(agent_id, model=model, llm_config=config)
-+ client.agents.update(agent_id, model=model, llm_config=config)
-
-# Message streaming
-- client.agents.messages.create_stream(agent_id, messages=messages)
-+ client.agents.messages.stream(agent_id, messages=messages)
-```
-
-
-### 4. Message Roles and Stop Reasons
-
-Enums replaced with string literals:
-
-
-```typescript TypeScript
-// Message roles
-- role: Letta.MessageCreateRole.User
-+ role: "user"
-
-// Stop reasons
-- if (stopReason === Letta.StopReasonType.EndTurn)
-+ if (stopReason === "end_turn")
-
-- if (stopReason === Letta.StopReasonType.RequiresApproval)
-+ if (stopReason === "requires_approval")
-```
-```python Python
-# Message roles
-- role=MessageRole.USER
-+ role="user"
-
-# Stop reasons
-- if stop_reason == StopReasonType.END_TURN:
-+ if stop_reason == "end_turn":
-
-- if stop_reason == StopReasonType.REQUIRES_APPROVAL:
-+ if stop_reason == "requires_approval":
-```
-
-
-### 5. Tool Calls Structure
-
-Tool calls changed from single object to array:
-
-
-```typescript TypeScript
-// Old - single tool_call
-- if (message.messageType === "approval_request_message") {
-- const toolCall = message.toolCall;
-- const id = toolCall.toolCallId;
-- const name = toolCall.name;
-- }
-
-// New - tool_calls array
-+ if (message.message_type === "approval_request_message") {
-+ const toolCalls = message.tool_calls || [];
-+ if (toolCalls.length > 0) {
-+ const toolCall = toolCalls[0];
-+ const id = toolCall.tool_call_id;
-+ const name = toolCall.name;
-+ }
-+ }
-```
-```python Python
-# Old - single tool_call
-- if message.message_type == "approval_request_message":
-- tool_call = message.tool_call
-- id = tool_call.tool_call_id
-- name = tool_call.name
-
-# New - tool_calls array
-+ if message.message_type == "approval_request_message":
-+ tool_calls = message.tool_calls or []
-+ if len(tool_calls) > 0:
-+ tool_call = tool_calls[0]
-+ id = tool_call.tool_call_id
-+ name = tool_call.name
-```
-
-
-### 6. Pagination
-
-List methods now return page objects:
-
-
-```typescript TypeScript
-// Old
-- const messages = await client.agents.messages.list(agentId);
-
-// New
-+ const messagesPage = await client.agents.messages.list(agentId);
-+ const messages = messagesPage.items;
-```
-```python Python
-# Old
-- messages = client.agents.messages.list(agent_id=agent_id)
-
-# New
-+ messages_page = client.agents.messages.list(agent_id=agent_id)
-+ messages = messages_page.items
-```
-
-
-### 7. Date Handling
-
-
-```typescript TypeScript
-// Old
-- date: new Date()
-
-// New
-+ date: new Date().toISOString()
-```
-```python Python
-# Python handles this automatically
-from datetime import datetime
-date = datetime.now() # Works in both versions
-```
-
-
-### 8. Archive Management (New APIs)
-
-New endpoints for managing archival memory:
-
-
-```typescript TypeScript
-// Create archive
-const archive = await client.archives.create({
- name: "my-archive",
- description: "Project knowledge base"
-});
-
-// List archives
-const archives = await client.archives.list();
-
-// Get archive by ID
-const archive = await client.archives.retrieve(archiveId);
-
-// Update archive
-await client.archives.update(archiveId, { name: "updated-name" });
-
-// Delete archive
-await client.archives.delete(archiveId);
-
-// Attach archive to agent
-await client.agents.archives.attach(agentId, archiveId);
-
-// Detach archive from agent
-await client.agents.archives.detach(agentId, archiveId);
-
-// List agents using an archive
-const agents = await client.archives.agents.list(archiveId);
-
-// Manage memories in archive
-await client.archives.memories.create(archiveId, { text: "Important fact" });
-await client.archives.memories.update(archiveId, memoryId, { text: "Updated fact" });
-await client.archives.memories.delete(archiveId, memoryId);
-```
-```python Python
-# Create archive
-archive = client.archives.create(
- name="my-archive",
- description="Project knowledge base"
-)
-
-# Attach/detach
-client.agents.archives.attach(agent_id, archive_id)
-client.agents.archives.detach(agent_id, archive_id)
-
-# Manage memories
-client.archives.memories.create(archive_id, text="Important fact")
-```
-
-
-### 9. Identity and Block Management
-
-New attach/detach patterns for identities and blocks:
-
-
-```typescript TypeScript
-// Attach identity to agent
-await client.agents.identities.attach(agentId, identityId);
-
-// Detach identity from agent
-await client.agents.identities.detach(agentId, identityId);
-
-// Attach identity to block
-await client.blocks.identities.attach(blockId, identityId);
-
-// Detach identity from block
-await client.blocks.identities.detach(blockId, identityId);
-
-// Agent now returns full identity objects
-const agent = await client.agents.retrieve(agentId);
-// Old: agent.identity_ids = ["id1", "id2"]
-// New: agent.identities = [{ id: "id1", name: "Alice", ... }, ...]
-```
-```python Python
-# Attach/detach identities
-client.agents.identities.attach(agent_id, identity_id)
-client.agents.identities.detach(agent_id, identity_id)
-
-# Full identity objects
-agent = client.agents.retrieve(agent_id)
-for identity in agent.identities:
- print(identity.name)
-```
-
-
-### 10. Agent Configuration Updates
-
-New parameters available for agent creation and updates:
-
-
-```typescript TypeScript
-// Temperature, top_p, reasoning_effort now available at top level
-const agent = await client.agents.create({
- model: "openai/gpt-4",
- temperature: 0.7,
- top_p: 0.9,
- reasoning_effort: "medium",
- max_tokens: 4096,
- context_window_limit: 128000
-});
-
-// Update agent configuration
-await client.agents.update(agentId, {
- temperature: 0.5,
- context_window_limit: 64000
-});
-```
-```python Python
-# Create with configuration
-agent = client.agents.create(
- model="openai/gpt-4",
- temperature=0.7,
- top_p=0.9,
- reasoning_effort="medium",
- max_tokens=4096,
- context_window_limit=128000
-)
-
-# Update configuration
-client.agents.update(
- agent_id,
- temperature=0.5
-)
-```
-
-
-### 11. Message Input Shorthand
-
-Simplified syntax for sending simple user messages:
-
-
-```typescript TypeScript
-// Old - verbose
-const response = await client.agents.messages.create(agentId, {
- messages: [{
- role: "user",
- content: "Hello!"
- }]
-});
-
-// New - shorthand available
-const response = await client.agents.messages.create(agentId, {
- input: "Hello!" // Automatically creates user message
-});
-
-// Both forms still supported
-```
-```python Python
-# Old
-response = client.agents.messages.create(
- agent_id,
- messages=[{"role": "user", "content": "Hello!"}]
-)
-
-# New shorthand
-response = client.agents.messages.create(
- agent_id,
- input="Hello!"
-)
-```
-
-
-### 12. Attach/Detach Return Values
-
-All attach/detach endpoints now return `None` instead of agent/object state:
-
-
-```typescript TypeScript
-// Old - returned updated agent
-const updatedAgent = await client.agents.tools.attach(agentId, toolId);
-
-// New - returns void/None
-await client.agents.tools.attach(agentId, toolId);
-// Fetch agent separately if needed
-const agent = await client.agents.retrieve(agentId);
-```
-```python Python
-# Old
-updated_agent = client.agents.tools.attach(agent_id, tool_id)
-
-# New
-client.agents.tools.attach(agent_id, tool_id)
-agent = client.agents.retrieve(agent_id)
-```
-
-
-**Affected methods:**
-- `agents.tools.attach/detach`
-- `agents.blocks.attach/detach`
-- `agents.folders.attach/detach`
-- `agents.archives.attach/detach`
-- `agents.identities.attach/detach`
-- `blocks.identities.attach/detach`
-
-### 13. Agent Import/Export Changes
-
-Import endpoint now supports name overriding:
-
-
-```typescript TypeScript
-// Old - append_copy_suffix parameter
-const agent = await client.agents.import(file, {
- append_copy_suffix: true // Deprecated
-});
-
-// New - override_name parameter
-const agent = await client.agents.import(file, {
- override_name: "my-imported-agent" // Optional, exact name to use
-});
-```
-```python Python
-# Old
-agent = client.agents.import_agent(
- file,
- append_copy_suffix=True
-)
-
-# New
-agent = client.agents.import_agent(
- file,
- override_name="my-imported-agent"
-)
-```
-
-
-### 14. Query Parameter to Request Body Changes
-
-Several endpoints moved from query parameters to request body:
-
-
-```typescript TypeScript
-// Tool approval settings (was query params, now request body)
-await client.agents.tools.update_approval(agentId, toolName, {
- require_approval: true
-});
-
-// Reset messages (was query param, now request body)
-await client.agents.messages.reset(agentId, {
- add_default_initial_messages: false
-});
-
-// Steps feedback (was query params, now request body)
-await client.steps.feedback.create(stepId, {
- rating: "positive",
- tags: ["helpful", "accurate"]
-});
-```
-```python Python
-# Tool approval
-client.agents.tools.update_approval(
- agent_id,
- tool_name,
- require_approval=True
-)
-
-# Reset messages
-client.agents.messages.reset(
- agent_id,
- add_default_initial_messages=False
-)
-```
-
-
-### 15. Tags Endpoint
-
-Tags list endpoint now uses `name` parameter instead of `query_text`:
-
-
-```typescript TypeScript
-// Old
-const tags = await client.tags.list({ query_text: "important" });
-
-// New
-const tags = await client.tags.list({ name: "important" });
-```
-```python Python
-# Old
-tags = client.tags.list(query_text="important")
-
-# New
-tags = client.tags.list(name="important")
-```
-
-
-### 16. Project ID Handling
-
-Project ID is now passed in the client constructor or headers:
-
-
-```typescript TypeScript
-// Pass in constructor
-const client = new Letta({
- apiKey: process.env.LETTA_API_KEY,
- projectId: "proj_123"
-});
-
-// No longer in URL paths
-- await client.templates.agents.create("proj_123", templateVersion, data)
-+ await client.templates.agents.create(templateVersion, data)
-```
-```python Python
-# Pass in constructor
-client = Letta(
- api_key=os.environ["LETTA_API_KEY"],
- project_id="proj_123"
-)
-```
-
-
-### 17. MCP (Model Context Protocol) Server Management
-
-MCP routes have been completely restructured with new endpoints under `/mcp-servers`:
-
-
-```typescript TypeScript
-// OLD ROUTES (under /tools/mcp/servers - DEPRECATED)
-// Using server names and tool names
-
-// List MCP servers
-- const servers = await client.tools.mcp.servers.list();
-
-// Add MCP server
-- await client.tools.mcp.servers.create(serverConfig);
-
-// Update MCP server by name
-- await client.tools.mcp.servers.update(serverName, updateConfig);
-
-// Delete MCP server by name
-- await client.tools.mcp.servers.delete(serverName);
-
-// List tools from a server by name
-- const tools = await client.tools.mcp.servers.tools.list(serverName);
-
-// Add individual tool by name
-- await client.tools.mcp.servers.tools.add(serverName, toolName);
-
-// Resync tools
-- await client.tools.mcp.servers.resync(serverName);
-
-// Execute tool by names
-- await client.tools.mcp.servers.tools.execute(serverName, toolName, { args });
-
-// Connect to server (for OAuth)
-- await client.tools.mcp.servers.connect(serverConfig);
-
-// NEW ROUTES (under /mcp-servers)
-// Using server IDs and tool IDs
-
-// List MCP servers (returns array of server objects with IDs)
-+ const servers = await client.mcp_servers.list();
-
-// Create MCP server (automatically syncs tools)
-+ const server = await client.mcp_servers.create(serverConfig);
-+ // Returns: { id: "mcp_server_123", name: "...", ... }
-
-// Get MCP server by ID
-+ const server = await client.mcp_servers.retrieve(serverId);
-
-// Update MCP server by ID
-+ await client.mcp_servers.update(serverId, updateConfig);
-
-// Delete MCP server by ID
-+ await client.mcp_servers.delete(serverId);
-
-// List tools from a server by ID
-+ const tools = await client.mcp_servers.tools.list(serverId);
-
-// Get specific tool by ID
-+ const tool = await client.mcp_servers.tools.retrieve(serverId, toolId);
-
-// Run/execute tool by ID
-+ const result = await client.mcp_servers.tools.run(serverId, toolId, {
-+ args: { key: "value" }
-+ });
-
-// Refresh tools (replaces resync)
-+ await client.mcp_servers.refresh(serverId);
-
-// Connect to server (for OAuth) - now uses server ID
-+ await client.mcp_servers.connect(serverId);
-```
-```python Python
-# OLD ROUTES (DEPRECATED)
-- servers = client.tools.mcp.servers.list()
-- client.tools.mcp.servers.create(server_config)
-- client.tools.mcp.servers.update(server_name, update_config)
-- client.tools.mcp.servers.delete(server_name)
-- tools = client.tools.mcp.servers.tools.list(server_name)
-- client.tools.mcp.servers.tools.add(server_name, tool_name)
-- client.tools.mcp.servers.resync(server_name)
-- client.tools.mcp.servers.tools.execute(server_name, tool_name, args=args)
-
-# NEW ROUTES
-+ servers = client.mcp_servers.list()
-+ server = client.mcp_servers.create(server_config)
-+ server = client.mcp_servers.retrieve(server_id)
-+ client.mcp_servers.update(server_id, update_config)
-+ client.mcp_servers.delete(server_id)
-+ tools = client.mcp_servers.tools.list(server_id)
-+ tool = client.mcp_servers.tools.retrieve(server_id, tool_id)
-+ result = client.mcp_servers.tools.run(server_id, tool_id, args={"key": "value"})
-+ client.mcp_servers.refresh(server_id)
-+ client.mcp_servers.connect(server_id)
-```
-
-
-**Key Changes:**
-- **Namespace**: Moved from `client.tools.mcp.servers` to `client.mcp_servers`
-- **Identification**: Use server IDs and tool IDs instead of names
- - Old: `serverName` (string) → New: `serverId` (ID string like `"mcp_server_123"`)
- - Old: `toolName` (string) → New: `toolId` (ID string like `"tool_456"`)
-- **Tool Management**: Tools are now automatically synced when creating a server
- - No longer need to manually "add" individual tools
- - Use `refresh()` to resync tools (replaces `resync()`)
-- **Tool Execution**: Method renamed from `execute()` to `run()`
-- **Server Retrieval**: New `retrieve()` method to get individual server by ID
-- **Tool Retrieval**: New `retrieve()` method to get individual tool by ID
-
-**Migration Example:**
-
-
-```typescript TypeScript
-// Before: Using names
-const servers = await client.tools.mcp.servers.list();
-const myServer = servers.find(s => s.name === "my-server");
-const tools = await client.tools.mcp.servers.tools.list("my-server");
-const myTool = tools.find(t => t.name === "my-tool");
-await client.tools.mcp.servers.tools.execute("my-server", "my-tool", {
- args: { query: "hello" }
-});
-
-// After: Using IDs
-const servers = await client.mcp_servers.list();
-const myServer = servers.find(s => s.name === "my-server");
-const serverId = myServer.id; // Get ID from server object
-const tools = await client.mcp_servers.tools.list(serverId);
-const myTool = tools.find(t => t.name === "my-tool");
-const toolId = myTool.id; // Get ID from tool object
-await client.mcp_servers.tools.run(serverId, toolId, {
- args: { query: "hello" }
-});
-```
-```python Python
-# Before
-servers = client.tools.mcp.servers.list()
-my_server = next(s for s in servers if s.name == "my-server")
-tools = client.tools.mcp.servers.tools.list("my-server")
-my_tool = next(t for t in tools if t.name == "my-tool")
-client.tools.mcp.servers.tools.execute(
- "my-server",
- "my-tool",
- args={"query": "hello"}
-)
-
-# After
-servers = client.mcp_servers.list()
-my_server = next(s for s in servers if s.name == "my-server")
-server_id = my_server.id
-tools = client.mcp_servers.tools.list(server_id)
-my_tool = next(t for t in tools if t.name == "my-tool")
-tool_id = my_tool.id
-client.mcp_servers.tools.run(
- server_id,
- tool_id,
- args={"query": "hello"}
-)
-```
-
-
-**Notes:**
-- MCP servers and tools now have persistent IDs in the database
-- Server names are no longer unique identifiers - use IDs instead
-- Tool schemas are automatically kept in sync via the `refresh()` endpoint
-- The old routes under `/tools/mcp/servers` are deprecated and will be removed
-
-## Migration Examples
-
-### Complete Agent Creation
-
-
-```typescript TypeScript
-// Before
-const agent = await client.agents.create({
- agentType: Letta.AgentType.LettaV1Agent,
- model: "openai/gpt-4",
- contextWindowLimit: 200_000,
- blockIds: ["block-1", "block-2"],
- includeBaseTools: false,
- includeBaseToolRules: false,
- initialMessageSequence: [],
-});
-
-// After
-const agent = await client.agents.create({
- agent_type: "letta_v1_agent" as AgentType,
- model: "openai/gpt-4",
- context_window_limit: 200_000,
- block_ids: ["block-1", "block-2"],
- include_base_tools: false,
- include_base_tool_rules: false,
- initial_message_sequence: [],
-});
-```
-```python Python
-# Before
-agent = client.agents.create(
- agent_type=AgentType.LETTA_V1_AGENT,
- model="openai/gpt-4",
- context_window_limit=200_000,
- block_ids=["block-1", "block-2"],
- include_base_tools=False,
- include_base_tool_rules=False,
- initial_message_sequence=[],
-)
-
-# After
-agent = client.agents.create(
- agent_type="letta_v1_agent",
- model="openai/gpt-4",
- context_window_limit=200_000,
- block_ids=["block-1", "block-2"],
- include_base_tools=False,
- include_base_tool_rules=False,
- initial_message_sequence=[],
-)
-```
-
-
-### Streaming Messages
-
-
-```typescript TypeScript
-// Before
-const stream = await client.agents.messages.createStream(agentId, {
- messages: [{
- role: Letta.MessageCreateRole.User,
- content: "Hello"
- }],
- streamTokens: true,
-});
-
-// After
-const stream = await client.agents.messages.stream(agentId, {
- messages: [{
- role: "user",
- content: "Hello"
- }],
- stream_tokens: true,
-});
-```
-```python Python
-# Before
-stream = client.agents.messages.create_stream(
- agent_id=agent_id,
- messages=[{"role": "user", "content": "Hello"}],
- stream_tokens=True,
-)
-
-# After
-stream = client.agents.messages.stream(
- agent_id=agent_id,
- messages=[{"role": "user", "content": "Hello"}],
- stream_tokens=True,
-)
-```
-
-
-### Handling Approvals
-
-
-```typescript TypeScript
-// Before
-if (message.messageType === "approval_request_message") {
- const toolCall = message.toolCall;
- await client.agents.messages.create(agentId, {
- messages: [{
- type: "approval",
- approvalRequestId: toolCall.toolCallId,
- approve: true,
- }],
- });
-}
-
-// After
-if (message.message_type === "approval_request_message") {
- const toolCalls = message.tool_calls || [];
- if (toolCalls.length > 0) {
- const toolCall = toolCalls[0];
- await client.agents.messages.create(agentId, {
- messages: [{
- type: "approval",
- approval_request_id: toolCall.tool_call_id,
- approve: true,
- }],
- });
- }
-}
-```
-```python Python
-# Before
-if message.message_type == "approval_request_message":
- tool_call = message.tool_call
- client.agents.messages.create(
- agent_id=agent_id,
- messages=[{
- "type": "approval",
- "approval_request_id": tool_call.tool_call_id,
- "approve": True,
- }],
- )
-
-# After
-if message.message_type == "approval_request_message":
- tool_calls = message.tool_calls or []
- if len(tool_calls) > 0:
- tool_call = tool_calls[0]
- client.agents.messages.create(
- agent_id=agent_id,
- messages=[{
- "type": "approval",
- "approval_request_id": tool_call.tool_call_id,
- "approve": True,
- }],
- )
-```
-
-
-### Updating Agent Configuration
-
-
-```typescript TypeScript
-// Before
-await client.agents.modify(agentId, {
- model: "openai/gpt-4",
- llmConfig: { temperature: 0.7 }
-});
-const agent = await client.agents.retrieve(agentId);
-const config = agent.llmConfig;
-
-// After
-await client.agents.update(agentId, {
- model: "openai/gpt-4",
- llm_config: { temperature: 0.7 }
-});
-const agent = await client.agents.retrieve(agentId);
-const config = agent.llm_config;
-```
-```python Python
-# Before
-client.agents.modify(
- agent_id=agent_id,
- model="openai/gpt-4",
- llm_config={"temperature": 0.7}
-)
-agent = client.agents.retrieve(agent_id=agent_id)
-config = agent.llm_config
-
-# After
-client.agents.update(
- agent_id=agent_id,
- model="openai/gpt-4",
- llm_config={"temperature": 0.7}
-)
-agent = client.agents.retrieve(agent_id=agent_id)
-config = agent.llm_config
-```
-
-
-## Migration Checklist
-
-Use this checklist to ensure a complete migration:
-
-**Core SDK Changes:**
-- [ ] Update package version to `1.0.0-alpha.10` or later
-- [ ] Update all imports (client and types)
-- [ ] Replace `LettaClient` with `Letta`
-- [ ] Update client constructor params: `token` → `apiKey`, `baseUrl` → `baseURL`
-- [ ] Add `projectId` to client constructor if using multi-project setup
-- [ ] Convert all property names from `camelCase` to `snake_case`
-- [ ] Replace enum references with string literals
-- [ ] Convert `Date` objects to ISO strings where required
-- [ ] Update type annotations to use new import paths
-
-**Method Renames:**
-- [ ] Update `modify()` calls to `update()`
-- [ ] Update `createStream()` calls to `stream()`
-- [ ] Rename `summarize_agent_conversation()` → `messages.summarize()`
-- [ ] Rename `cancel_agent_run()` → `messages.cancel()`
-- [ ] Rename `preview_raw_payload()` → `messages.preview()`
-- [ ] Rename `list_agent_files()` → `files.list()`
-- [ ] Rename `export_agent_serialized()` → `export()`
-- [ ] Rename `import_agent_serialized()` → `import()`
-- [ ] Rename folder/provider method names (see section 2)
-- [ ] Update telemetry routes to use `steps.trace()`
-
-**Pagination:**
-- [ ] Update all list methods to access `.items` property
-- [ ] Replace `sort_by` with `order_by` in `agents.list()`
-- [ ] Replace `ascending` with `order` parameter
-- [ ] Update pagination parameters: `before`, `after`, `limit`, `order`
-- [ ] Handle cursor-based pagination for all list endpoints
-
-**Message Handling:**
-- [ ] Handle `tool_calls` as an array instead of single object
-- [ ] Update `identity_ids` references to use `identities` (full objects)
-- [ ] Replace `agent.memory` with `agent.blocks`
-- [ ] Update `step.messages` to use `steps.messages.list()`
-- [ ] Consider using new `input` shorthand for simple messages
-
-**Deprecations:**
-- [ ] Remove usage of deprecated search endpoints
-- [ ] Replace `list_active()` with `list(active=True)`
-- [ ] Remove `use_assistant_message` parameter
-- [ ] Replace `tool_exec_environment_variables` with `secrets`
-- [ ] Remove template-related fields from agent/block objects
-- [ ] Replace sources endpoints with folders
-- [ ] Replace passages endpoints with archives
-
-**New Features:**
-- [ ] Update attach/detach methods (now return `None`)
-- [ ] Use new archive management APIs if needed
-- [ ] Update agent import to use `override_name` instead of `append_copy_suffix`
-- [ ] Move query parameters to request body for affected endpoints
-- [ ] Use new agent configuration parameters (`temperature`, `top_p`, etc.)
-
-**MCP (Model Context Protocol) Changes:**
-- [ ] Migrate from `client.tools.mcp.servers` to `client.mcp_servers`
-- [ ] Update MCP server references to use IDs instead of names
-- [ ] Update MCP tool references to use IDs instead of names
-- [ ] Remove manual tool "add" operations (tools auto-sync on server create)
-- [ ] Replace `resync()` calls with `refresh()`
-- [ ] Replace `execute()` calls with `run()`
-- [ ] Add server/tool ID lookup logic if using names
-- [ ] Update OAuth connection flow to use server IDs
-
-**Testing:**
-- [ ] Test all agent operations (create, update, message)
-- [ ] Test streaming and approval flows
-- [ ] Verify memory block operations still work
-- [ ] Test pagination on list endpoints
-- [ ] Test archive management if used
-- [ ] Verify identity/block attach/detach operations
-- [ ] Test agent import/export
-
-## Automated Migration Tools
-
-### Find and Replace Script
-
-Use this script to help automate common replacements:
-
-
-```bash Shell (TypeScript projects)
-# Install dependencies
-npm install -g jscodeshift
-
-# Run find-and-replace (adjust paths as needed)
-find src -name "*.ts" -o -name "*.tsx" | xargs sed -i '' \
- -e 's/LettaClient/Letta/g' \
- -e 's/\.modify(/.update(/g' \
- -e 's/\.createStream(/.stream(/g' \
- -e 's/\.messageType/.message_type/g' \
- -e 's/\.toolCall/.tool_calls/g' \
- -e 's/\.toolCallId/.tool_call_id/g' \
- -e 's/\.toolReturn/.tool_return/g' \
- -e 's/llmConfig/llm_config/g' \
- -e 's/streamTokens/stream_tokens/g' \
- -e 's/\.tools\.mcp\.servers/\.mcp_servers/g' \
- -e 's/\.resync(/\.refresh(/g'
-
-# Note: MCP server/tool name -> ID migration requires manual intervention
-# as you need to fetch IDs from the API
-```
-```python Python (migration helper)
-import re
-from pathlib import Path
-
-def migrate_file(filepath: Path):
- """Apply SDK v1.0 migration patterns to a Python file"""
- content = filepath.read_text()
-
- # Import updates
- content = re.sub(
- r'from letta_client import (\w+)',
- r'from letta import \1',
- content
- )
-
- # Method renames
- content = content.replace('.modify(', '.update(')
- content = content.replace('.create_stream(', '.stream(')
-
- # MCP namespace changes
- content = content.replace('.tools.mcp.servers', '.mcp_servers')
- content = content.replace('.resync(', '.refresh(')
-
- # Already using snake_case in Python, but fix any camelCase
- content = re.sub(r'messageType', 'message_type', content)
- content = re.sub(r'toolCall([^_])', r'tool_calls\1', content)
-
- filepath.write_text(content)
- print(f"✓ Migrated {filepath}")
-
-# Usage
-for py_file in Path('src').rglob('*.py'):
- migrate_file(py_file)
-```
-
-
-
-**Always review automated changes!** These scripts help with common patterns but cannot handle all edge cases. Test thoroughly after migration.
-
-
-## Troubleshooting
-
-### "Property 'llmConfig' does not exist" (TypeScript)
-
-**Cause:** Property renamed to `llm_config`
-
-**Fix:** Update all references to use snake_case
-
-```typescript
-- agent.llmConfig
-+ agent.llm_config
-```
-
-### "Cannot read property 'toolCallId' of undefined"
-
-**Cause:** `tool_call` changed to `tool_calls` (array)
-
-**Fix:** Access the first element of the array
-
-```typescript
-- const id = message.toolCall.toolCallId;
-+ const toolCalls = message.tool_calls || [];
-+ const id = toolCalls[0]?.tool_call_id;
-```
-
-### "items is not iterable"
-
-**Cause:** Trying to iterate over page object instead of items array
-
-**Fix:** Access the `.items` property first
-
-```typescript
-- for (const message of messages) {
-+ const messagesPage = await client.agents.messages.list(agentId);
-+ for (const message of messagesPage.items) {
-```
-
-### "Cannot find module '@letta-ai/letta-client/resources/...'"
-
-**Cause:** Types moved to subpath exports
-
-**Fix:** Update imports to use new subpaths
-
-```typescript
-- import { Letta } from "@letta-ai/letta-client";
-+ import type { Block } from "@letta-ai/letta-client/resources/agents/agents";
-```
-
-### "Method 'modify' does not exist"
-
-**Cause:** Method renamed to `update`
-
-**Fix:** Update all modify calls
-
-```typescript
-- await client.agents.modify(agentId, updates)
-+ await client.agents.update(agentId, updates)
-```
-
-### "Cannot access property 'identity_ids'"
-
-**Cause:** Field renamed to `identities` and now returns full objects
-
-**Fix:** Access the `identities` array and extract IDs if needed
-
-```typescript
-- const ids = agent.identity_ids;
-+ const identities = agent.identities;
-+ const ids = identities.map(i => i.id);
-```
-
-### "Pagination parameters 'sort_by' or 'ascending' not recognized"
-
-**Cause:** Pagination parameters standardized to `order_by` and `order`
-
-**Fix:** Update parameter names
-
-```typescript
-- await client.agents.list({ sort_by: "created_at", ascending: true })
-+ await client.agents.list({ order_by: "created_at", order: "asc" })
-```
-
-### "Attach/detach methods return undefined"
-
-**Cause:** These methods now return `None`/`void` instead of updated state
-
-**Fix:** Fetch the object separately if you need the updated state
-
-```typescript
-await client.agents.tools.attach(agentId, toolId);
-const agent = await client.agents.retrieve(agentId); // Get updated state
-```
-
-### "Cannot find method 'summarize_agent_conversation'"
-
-**Cause:** Method moved to messages subresource
-
-**Fix:** Use the new path
-
-```typescript
-- await client.agents.summarize_agent_conversation(agentId)
-+ await client.agents.messages.summarize(agentId)
-```
-
-### "Query parameter 'add_default_initial_messages' not working"
-
-**Cause:** Parameter moved from query to request body
-
-**Fix:** Pass as request body parameter
-
-```typescript
-- await client.agents.messages.reset(agentId, { params: { add_default_initial_messages: false } })
-+ await client.agents.messages.reset(agentId, { add_default_initial_messages: false })
-```
-
-### "Cannot find 'client.tools.mcp.servers'"
-
-**Cause:** MCP routes moved to new namespace
-
-**Fix:** Use new MCP server methods
-
-```typescript
-- await client.tools.mcp.servers.list()
-+ await client.mcp_servers.list()
-```
-
-### "MCP server not found by name"
-
-**Cause:** MCP methods now use server IDs instead of names
-
-**Fix:** Lookup server ID from name first
-
-```typescript
-// Get server ID from name
-const servers = await client.mcp_servers.list();
-const myServer = servers.find(s => s.name === "my-server");
-const serverId = myServer.id;
-
-// Use ID for subsequent operations
-await client.mcp_servers.tools.list(serverId);
-```
-
-### "MCP tool 'toolName' not found"
-
-**Cause:** MCP tool execution now uses tool IDs instead of names
-
-**Fix:** Lookup tool ID from name first
-
-```typescript
-const tools = await client.mcp_servers.tools.list(serverId);
-const myTool = tools.find(t => t.name === "my-tool");
-const toolId = myTool.id;
-
-await client.mcp_servers.tools.run(serverId, toolId, { args });
-```
-
-### "Method 'execute' not found on mcp_servers.tools"
-
-**Cause:** Method renamed from `execute()` to `run()`
-
-**Fix:** Use the new method name
-
-```typescript
-- await client.mcp_servers.tools.execute(serverId, toolId, { args })
-+ await client.mcp_servers.tools.run(serverId, toolId, { args })
-```
-
-## Additional Resources
-
-- [Architecture Migration Guide](/guides/legacy/migration_guide) - For migrating agent architectures
-- [API Reference](/api-reference) - Complete SDK documentation
-- [Changelog](/api-reference/changelog) - All SDK changes
-- [GitHub](https://github.com/letta-ai/letta) - Source code and issues
-- [Discord](https://discord.gg/letta) - Get help from the community
-
-## Getting Help
-
-If you encounter issues during migration:
-
-1. **Check the [Changelog](/api-reference/changelog)** for detailed release notes
-2. **Search [GitHub Issues](https://github.com/letta-ai/letta/issues)** for known problems
-3. **Ask in [Discord #dev-help](https://discord.gg/letta)** for community support
-4. **Contact support@letta.com** for enterprise support
diff --git a/fern/pages/cloud/api_keys.mdx b/fern/pages/cloud/api_keys.mdx
deleted file mode 100644
index b7a5c36c..00000000
--- a/fern/pages/cloud/api_keys.mdx
+++ /dev/null
@@ -1,28 +0,0 @@
----
-title: Bring-Your-Own API Keys
-subtitle: Connect your own API keys for supported model providers (OpenAI, Anthropic, etc.)
-slug: guides/cloud/custom-keys
----
-
-
-To generate a **Letta API key** (which you use to interact with your agents on Letta Cloud), visit your [account settings](https://app.letta.com/settings/profile) page.
-
-
-
-Letta Cloud only support bring-your-own-key for enterprise customers. To learn more about enterprise plans and pricing, visit our [pricing page](https://www.letta.com/pricing) or [contact us](https://forms.letta.com/request-demo) to request a demo.
-
-
-## Using Your Own API Keys
-
-Connect your own API keys for supported providers (OpenAI, Anthropic, Gemini) to Letta Cloud through the [models page](https://app.letta.com/models). When you have a custom API key (successfully) registered, you will see additional models listed in the ADE model dropdown.
-
-### Selecting Your Custom Provider
-
-After you connect your own OpenAI / Anthropic / Gemini API key, make sure to select your custom provider in the ADE under "Your models".
-For example, after connecting your own OpenAI API key, you will see multiple OpenAI models but with different providers ("Letta hosted" vs "Your models") - if you want to use your own OpenAI API key, you need to select the copy of the model associated with your custom provider.
-
-### Billing and Quotas
-
-Requests made using your custom API keys **do not count** towards your monthly request quotas or usage-based billing. Instead, you'll be billed directly by the provider (OpenAI, Anthropic, etc.) according to their pricing for your personal account.
-
-Note that direct provider pricing will likely differ from Letta Cloud rates, and requests through your own API key may cost more than those made through Letta Cloud's managed services.
diff --git a/fern/pages/cloud/observability.mdx b/fern/pages/cloud/observability.mdx
deleted file mode 100644
index 6c9756a4..00000000
--- a/fern/pages/cloud/observability.mdx
+++ /dev/null
@@ -1,31 +0,0 @@
----
-title: "Observability Overview"
-subtitle: "Monitor and trace your agents in Letta Cloud"
-slug: "guides/observability"
----
-
-
-All observability features are available in real-time for every Letta Cloud project.
-
-
-Letta Cloud's observability tools help you monitor performance and debug issues. Each project you create in Letta Cloud has two main observability dashboards:
-
-## [Monitoring](/guides/cloud/monitoring)
-
-
-
-
-Track key metrics across four dashboards:
-- **Overview**: Message count, API/tool errors, LLM/tool latency
-- **Activity & Usage**: Usage patterns and resource consumption
-- **Performance**: Response times and throughput
-- **Errors**: Detailed error analysis and debugging info
-
-## [Responses & Tracing](/guides/observability/responses)
-
-
-
-
-Inspect API responses and agent execution:
-- **API Responses**: List of all responses with duration and status
-- **Message Inspection**: Click "Inspect Message" to see the full POST request and agent loop execution sequence
diff --git a/fern/pages/cloud/pricing.mdx b/fern/pages/cloud/pricing.mdx
deleted file mode 100644
index 73ed9a50..00000000
--- a/fern/pages/cloud/pricing.mdx
+++ /dev/null
@@ -1,63 +0,0 @@
----
-title: Plans & Pricing
-subtitle: Guide to pricing and model usage for Free, Pro, and Enterprise plans
-slug: guides/cloud/plans
----
-
-
-Upgrade your plan and view your usage on [your account page](https://app.letta.com/settings/organization/usage)
-
-
-## Available Plans
-
-
-
- - **5,000** monthly credits
- - Access the Letta API
- - Edit agents visually in the ADE
- - **2** agent templates
- - **1 GB** of storage
-
-
- - **20,000** monthly credits
- - Pay-as-you-go credit overage
- - Unlimited agents
- - **20** agent templates
- - **10 GB** of storage
-
-
-
-
-For organizations with higher volume needs, our Enterprise plan offers increased quotas, dedicated support, role-based access control (RBAC), SSO (SAML, OIDC), and private model deployment options.
-[Contact our team](https://forms.letta.com/request-demo) to learn more.
-
-
-## What are credits?
-
-Credits are a standard cost unit for resources in Letta, such as LLM inference and CPU cycles. When agents run on Letta, they make LLM model requests and execute tools. Model requests consume credits at a rate depending on the model tier (standard vs. premium) and whether Max Mode is enabled for longer context sizes. Tool executions that run in Letta are charged at a flat rate per second of execution. See up-to-date credit pricing for available models [here](https://app.letta.com/settings/organization/models).
-
-## What tools are executed by Letta?
-
-Sandbox code execution and execution of custom tools run inside of Letta, so incur a credit cost for CPU time. Remote MCP tools are executed by the MCP tool provider, so do not have a credit cost. Letta built-in tools are executed for free.
-
-## How do monthly credits work?
-
-Your Letta agents use large language models (LLMs) to reason and take actions. These model requests consume credits from your monthly balance (or additional purchased credits). Your balance of monthly credits refreshes every month.
-
-## What is Max Mode?
-
-Certain models have the ability to run with extended context windows. Turning on Max Mode extends the context window of the model driving your Letta agent beyond the 100k default, which may help when working with large files or codebases, but will increase cost (credit use) and latency.
-
-## What's the difference between the Letta API and open source Letta?
-
-The Letta API Platform is our fully-managed service for stateful agents, handling all agent infrastructure and state management to create scalable agent services. The Letta API Platform also has additional features beyond the open source such as durable execution for long-running agents, built-in sandboxing, agent templates, optimized vector search, message indexing, and observability.
-
-## Can I transfer my agents between open source and cloud?
-
-Yes, the Letta API Platform supports [agent file](https://docs.letta.com/guides/agents/agent-file), which allows you to move your agents freely between self-hosted instances of the Letta open source and the Letta platform.
diff --git a/fern/pages/cloud/variables.mdx b/fern/pages/cloud/variables.mdx
deleted file mode 100644
index e96c7045..00000000
--- a/fern/pages/cloud/variables.mdx
+++ /dev/null
@@ -1,54 +0,0 @@
----
-title: Memory Variables
-slug: guides/templates/variables
----
-
-
-Memory variables are a feature in [agent templates](/guides/cloud/templates) (part of [Letta Cloud](/guides/cloud)).
-To use memory variables, you must be using an agent template, not an agent.
-
-
-Memory variables allow you to dynamically define parts of your agent memory at the time of agent creation (when a [template](/guides/cloud/templates) is used to create a new agent).
-
-## Defining variables in memory blocks
-
-To use memory variables in your agent templates, you can define variables in your memory blocks by wrapping them in `{{ }}`.
-For example, if you have an agent template called `customer-service-template` designed to handle customer support issues, you might have a block of memory that stores information about the user:
-```handlebars
-The user is contacting me to resolve a customer support issue.
-Their name is {{name}} and the ticket number for this request is {{ticket}}.
-```
-
-Once variables have been defined inside of your memory block, they will dynamically appear at variables in the **ADE variables window** (click the "\{\} Variables" button at the top of the chat window to expand the dropdown).
-
-## Simulating variable values in the ADE
-
-
-Reset the state of the simulated agent by clicking the "Flush Simulation" 🔄 button.
-
-
-While designing agent templates in the ADE, you can interact with a simulated agent.
-The ADE variables window allows you to specify the values of the variables for the simulated agent.
-
-You can see the current state of the simulated agent's memory by clicking the "Simulated" tab in the "Core Memory" panel in the ADE.
-If you're using memory variables and do not specify values for the variables in the ADE variables window, the simulated agent will use empty values.
-
-In this prior example, the `name` and `ticket` variables are memory variables that we will specify when we create a new agent - information that we expect to have available at that time.
-While designing the agent template, we will likely want to experiment with different values for these variables to make sure that the agent is behaving as expected.
-For example, if we change the name of the user from "Alice" to "Bob", the simulated agent should respond accordingly.
-
-## Defining variables during agent creation
-
-When we're ready to create an agent from our template, we can specify the values for the variables using the `variables` parameter in the [create agents from template endpoint](/api-reference/templates/agents/create):
-```sh
-curl -X POST https://app.letta.com/v1/templates/{project_slug}/{template_name}:{template_version} \
- -H 'Content-Type: application/json' \
- -H 'Authorization: Bearer YOUR_API_KEY' \
- -d '{
- "from_template": customer-service-template:latest",
- "variables": {
- "name": "Bob",
- "ticket": "TX-123"
- }
- }'
-```
diff --git a/fern/pages/cloud/versions.mdx b/fern/pages/cloud/versions.mdx
deleted file mode 100644
index 98c459df..00000000
--- a/fern/pages/cloud/versions.mdx
+++ /dev/null
@@ -1,41 +0,0 @@
----
-title: Versioning Agent Templates
-slug: guides/templates/versioning
----
-
-
-Versioning is a feature in [agent templates](/guides/cloud/templates) (part of [Letta Cloud](/guides/cloud/overview)).
-To use versioning, you must be using an agent template, not an agent.
-
-
-Versions allow you to keep track of the changes you've made to your template over time.
-Agent templates follow the versioning convention of `template-name:version-number`.
-
-Similar to [Docker tags](https://docs.docker.com/get-started/docker-concepts/building-images/build-tag-and-publish-an-image/#tagging-images), you can specify the latest version of a template using the `latest` keyword (`template-name:latest`).
-
-## Creating a new template version
-When you create a template, it starts off at version 1.
-Once you've make edits to your template in the ADE, you can create a new version of the template by clicking the "Template" button in the ADE (top right), then clicking "Save new template version".
-Version numbers are incremented automatically (e.g. version 1 becomes version 2).
-
-## Migrating existing agents to a new template version
-If you've deployed agents on a previous version of the template, you'll be asked if you want to migrate your existing agents to the new version of the template.
-When you migrate existing agents to a new template version, Letta Cloud will re-create your existing agents using the new template information, but keeping prior agent state such as the conversation history, and injecting memory variables as needed.
-
-### When should I migrate (or not migrate) my agents?
-One reason you might want to migrate your agents is if you've added new tools to your agent template: migrating existing agents to the new version of the template will give them access to the new tools, while retaining all of their prior state.
-Another example usecase is if you make modifications to your prompts to tune your agent behavior - if you find a modification works well, you can save a new version with the prompt edits, and migrate all deployed agents to the new version.
-
-### Forking an agent template
-If you decide to make significant changes to your agent and would prefer to make a new template to track your changes, you can easily create a new agent template from an existing template by **forking** your template (click the settings button ⚙️ in the ADE, then click "Fork Template").
-
-## Specifying a version when creating an agent
-
-You can specify a template version when creating an agent in the you can use the [create agents from template endpoint](/api-reference/templates/agents/create)
-For example, to deploy an agent from a template called `template-name` at version 2, you would use `:2` as the template tag:
-```sh
-curl -X POST https://app.letta.com/v1/templates/{project_slug}/{template_name}:2 \
- -H 'Content-Type: application/json' \
- -H 'Authorization: Bearer YOUR_API_KEY' \
- -d '{}'
-```
diff --git a/fern/pages/concepts/letta.mdx b/fern/pages/concepts/letta.mdx
deleted file mode 100644
index 08c6f3ca..00000000
--- a/fern/pages/concepts/letta.mdx
+++ /dev/null
@@ -1,124 +0,0 @@
----
-title: Research Background
-subtitle: The academic foundations of Letta
-slug: concepts/letta
----
-
-
-**Looking for practical concepts?** See [Core Concepts](/core-concepts) for understanding how to build with Letta's stateful agents.
-
-
-## Letta and MemGPT
-
-**[Letta](https://letta.com)** was created by the same team that created **[MemGPT](https://research.memgpt.ai)**.
-
-### MemGPT: The Research Paper
-
-**MemGPT is a research paper** ([arXiv:2310.08560](https://arxiv.org/abs/2310.08560)) that introduced foundational concepts for building stateful LLM agents:
-
-- **Self-editing memory** - LLMs using tools to edit their own context window and external storage
-- **LLM Operating System** - Infrastructure layer managing agent state, memory, and execution
-- **Memory hierarchy** - Distinguishing between in-context memory (core) and out-of-context memory (archival)
-- **Context window management** - Intelligent paging and memory consolidation techniques
-
-The paper demonstrated that LLMs could maintain coherent conversations far beyond their context window limits by actively managing their own memory through tool calling.
-
-[Read the full MemGPT paper →](https://arxiv.org/abs/2310.08560)
-
-### MemGPT: The Agent Architecture
-
-MemGPT also refers to a **specific agent architecture** popularized by the research paper. A MemGPT agent has:
-- Memory editing tools (`memory_replace`, `memory_insert`, `memory_rethink`)
-- Archival memory tools (`archival_memory_insert`, `archival_memory_search`)
-- Conversation search tools (`conversation_search`, `conversation_search_date`)
-- A structured context window with persona and human memory blocks
-
-This architecture makes MemGPT agents particularly effective for long-range chat applications, document search, and personalized assistants.
-
-[Learn more about MemGPT agents →](/guides/legacy/memgpt-agents-legacy)
-
-### Letta: The Framework
-
-**Letta is a production framework** that allows you to build and deploy agents with MemGPT-style memory systems (and beyond) as **services** behind REST APIs.
-
-While the MemGPT research focused on the agent architecture and memory system, Letta provides:
-- **Production infrastructure** - Database backends, persistence, state management
-- **Agent runtime** - Tool execution, reasoning loops, multi-agent orchestration
-- **Developer tools** - Agent Development Environment (ADE), SDKs, monitoring
-- **Deployment options** - Letta Cloud for managed hosting, or self-hosted with Docker
-- **Flexibility** - Build MemGPT agents, or design custom agent architectures with different memory systems
-
-**In short:**
-- **MemGPT (research)** = Ideas about how agents should manage memory
-- **MemGPT (architecture)** = Specific agent design with memory tools
-- **Letta (framework)** = Production system for building and deploying stateful agents
-
-## Agents in Context
-
-The concept of "agents" has a long history across multiple fields:
-
-**In reinforcement learning and AI**, agents are entities that:
-1. Perceive their environment through sensors
-2. Make decisions based on internal state
-3. Take actions that affect their environment
-4. Learn from outcomes to improve future decisions
-
-**In economics and game theory**, agents are autonomous decision-makers with their own objectives and strategies.
-
-**In LLMs**, agents extend these concepts by using language models for reasoning and tool calling for actions. Letta's approach emphasizes:
-- **Statefulness** - Persistent memory and identity across sessions
-- **Autonomy** - Self-directed memory management and multi-step reasoning
-- **Tool use** - Modifying internal state and accessing external resources
-
-## LLM Operating System
-
-The **LLM OS** is the infrastructure layer that manages agent execution and state. This concept, introduced in the MemGPT paper, draws an analogy to traditional operating systems:
-
-Just as an OS manages memory, processes, and I/O for programs, the LLM OS manages:
-- **Memory layer** - Context window management, paging, and persistence
-- **Agent runtime** - Tool execution and the reasoning loop
-- **Stateful layer** - Coordination across database, cache, and execution
-
-Letta implements this LLM OS architecture, providing the infrastructure for stateful agent services.
-
-## Self-Editing Memory
-
-A key innovation from the MemGPT research is **self-editing memory** - agents that actively manage their own memory using tools.
-
-Traditional RAG systems passively retrieve documents. Letta agents actively:
-- **Edit in-context memory** - Update memory blocks based on learned information
-- **Manage archival storage** - Decide what facts to persist long-term
-- **Search strategically** - Query their memory when relevant context is needed
-
-This active memory management enables agents to learn and evolve through interactions rather than requiring retraining or prompt engineering.
-
-[Learn more about Letta's memory system →](/guides/agents/memory)
-
-## Further Reading
-
-
-
- Practical guide to building with stateful agents
-
-
- Deep dive into the MemGPT paper's technical contributions
-
-
- How agents manage memory in Letta
-
-
- Build agents with the MemGPT architecture
-
-
diff --git a/fern/pages/concepts/memgpt.mdx b/fern/pages/concepts/memgpt.mdx
deleted file mode 100644
index 53c61dba..00000000
--- a/fern/pages/concepts/memgpt.mdx
+++ /dev/null
@@ -1,37 +0,0 @@
----
-title: MemGPT
-subtitle: Learn about the key ideas behind MemGPT
-slug: concepts/memgpt
----
-
-
-The MemGPT open source framework / package was renamed to _Letta_. You can read about the difference between Letta and MemGPT [here](/concepts/letta), or read more about the change on our [blog post](https://www.letta.com/blog/memgpt-and-letta).
-
-## MemGPT - the research paper
-
-
-
-
-
-**MemGPT** is the name of a [**research paper**](https://arxiv.org/abs/2310.08560) that popularized several of the key concepts behind the "LLM Operating System (OS)":
-1. **Memory management**: In MemGPT, an LLM OS moves data in and out of the context window of the LLM to manage its memory.
-2. **Memory hierarchy**: The "LLM OS" divides the LLM's memory (aka its "virtual context", similar to "[virtual memory](https://en.wikipedia.org/wiki/Virtual_memory)" in computer systems) into two parts: the in-context memory, and out-of-context memory.
-3. **Self-editing memory via tool calling**: In MemGPT, the "OS" that manages memory is itself an LLM. The LLM moves data in and out of the context window using designated memory-editing tools.
-4. **Multi-step reasoning using heartbeats**: MemGPT supports multi-step reasoning (allowing the agent to take multiple steps in sequence) via the concept of "heartbeats". Whenever the LLM outputs a tool call, it has to option to request a heartbeat by setting the keyword argument `request_heartbeat` to `true`. If the LLM requests a heartbeat, the LLM OS continues execution in a loop, allowing the LLM to "think" again.
-
-You can read more about the MemGPT memory hierarchy and memory management system in our [memory concepts guide](/advanced/memory_management).
-
-## MemGPT - the agent architecture
-
-**MemGPT** also refers to a particular **agent architecture** that was popularized by the paper and adopted widely by other LLM chatbots:
-1. **Chat-focused core memory**: The core memory of a MemGPT agent is split into two parts - the agent's own persona, and the user information. Because the MemGPT agent has self-editing memory, it can update its own personality over time, as well as update the user information as it learns new facts about the user.
-2. **Vector database archival memory**: By default, the archival memory connected to a MemGPT agent is backed by a vector database, such as [Chroma](https://www.trychroma.com/) or [pgvector](https://github.com/pgvector/pgvector). Because in MemGPT all connections to memory are driven by tools, it's simple to exchange archival memory to be powered by a more traditional database (you can even make archival memory a flatfile if you want!).
-
-## Creating MemGPT agents in the Letta framework
-
-Because **Letta** was created out of the original MemGPT open source project, it's extremely easy to make MemGPT agents inside of Letta (the default Letta agent architecture is a MemGPT agent).
-See our [agents overview](/guides/agents/overview) for a tutorial on how to create MemGPT agents with Letta.
-
-**The Letta framework also allow you to make agent architectures beyond MemGPT** that differ significantly from the architecture proposed in the research paper - for example, agents with multiple logical threads (e.g. a "concious" and a "subconcious"), or agents with more advanced memory types (e.g. task memory).
-
-Additionally, **the Letta framework also allows you to expose your agents as *services*** (over REST APIs) - so you can use the Letta framework to power your AI applications.
diff --git a/fern/pages/cookbooks/rag-agentic.mdx b/fern/pages/cookbooks/rag-agentic.mdx
deleted file mode 100644
index c45bba77..00000000
--- a/fern/pages/cookbooks/rag-agentic.mdx
+++ /dev/null
@@ -1,1540 +0,0 @@
----
-title: Agentic RAG with Letta
-subtitle: Empower your agent with custom search tools for autonomous retrieval
-slug: guides/rag/agentic
----
-
-In the Agentic RAG approach, we delegate the retrieval process to the agent itself. Instead of your application deciding what to search for, we provide the agent with a custom tool that allows it to query your vector database directly. This makes the agent more autonomous and your client-side code much simpler.
-
-By the end of this tutorial, you'll have a research assistant that autonomously decides when to search your vector database and what queries to use.
-
-## Prerequisites
-
-To follow along, you need free accounts for:
-
-- **[Letta](https://www.letta.com)** - To access the agent development platform
-- **[Hugging Face](https://huggingface.co/)** - For generating embeddings (MongoDB and Qdrant users only)
-- **One of the following vector databases:**
- - **[ChromaDB Cloud](https://www.trychroma.com/)** for a hosted vector database
- - **[MongoDB Atlas](https://www.mongodb.com/cloud/atlas/register)** for vector search with MongoDB
- - **[Qdrant Cloud](https://cloud.qdrant.io/)** for a high-performance vector database
-
-You will also need Python 3.8+ or Node.js v18+ and a code editor.
-
-
-**MongoDB and Qdrant users:** This guide uses Hugging Face's Inference API for generating embeddings. This approach keeps the tool code lightweight enough to run in Letta's sandbox environment.
-
-
-## Getting Your API Keys
-
-We'll need API keys for Letta and your chosen vector database.
-
-
-
-
-
- If you don't have one, sign up for a free account at [letta.com](https://www.letta.com).
-
-
- Once logged in, click on **API keys** in the sidebar.
- 
-
-
- Click **+ Create API key**, give it a descriptive name, and click **Confirm**. Copy the key and save it somewhere safe.
-
-
-
-
-
-
-
-
-
- Sign up for a free account on the [ChromaDB Cloud website](https://www.trychroma.com/).
-
-
- From your dashboard, create a new database.
- 
-
-
- In your project settings, you'll find your **API Key**, **Tenant**, **Database**, and **Host URL**. We'll need all of these for our scripts.
- 
-
-
-
-
-
-
-
- Sign up for a free account at [mongodb.com/cloud/atlas/register](https://www.mongodb.com/cloud/atlas/register).
-
-
- Click **Build a Cluster** and select the free tier (M0). Choose your preferred cloud provider and region and click **Create deployment**.
- 
-
-
- Next, set up connection security.
- 1. Create a database user, then click **Choose a connection method**
- 2. Choose **Drivers** to connect to your application, choose Python as the driver.
- 3. Copy the **entire** connection string, including the query parameters at the end. It will look like this:
-
- ```
- mongodb+srv://:@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
- ```
-
-
- Make sure to replace `` with your actual database user password. Keep all the query parameters (`?retryWrites=true&w=majority&appName=Cluster0`) they are required for proper connection configuration.
-
-
- 
-
-
- By default, MongoDB Atlas blocks all outside connections. You must grant access to the services that need to connect.
-
- 1. Navigate to **Database and Network Access** in the left sidebar.
- 2. Click **Add IP Address**.
- 3. For local development and testing, select **Allow Access From Anywhere**. This will add the IP address `0.0.0.0/0`.
- 4. Click **Confirm**.
-
- 
-
-
- For a production environment, you would replace `0.0.0.0/0` with a secure list of static IP addresses provided by your hosting service (e.g., Letta).
-
-
-
-
-
-
-
-
- Sign up for a free account at [cloud.qdrant.io](https://cloud.qdrant.io/).
-
-
- From your dashboard, click **Clusters** and then **+ Create**. Select the free tier and choose your preferred region.
-
- 
-
-
- Once your cluster is created, click on it to view details.
-
- Copy the following:
-
- 1. **API Key**
- 2. **Cluster URL**
-
- 
-
-
-
-
-
-
-
-
-
- Sign up for a free account at [huggingface.co](https://huggingface.co/join).
-
-
- Click the profile icon in the top right. Navigate to **Settings** > **Access Tokens** (or go directly to [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)).
-
-
- Click **New token**, give it a name (e.g., "Letta RAG Demo"), select **Read** role, and click **Create token**. Copy the token and save it securely.
- 
-
-
-
-
-The free tier includes 30,000 API requests per month, which is more than enough for development and testing.
-
-
-
-
-Once you have these credentials, create a `.env` file in your project directory. Add the credentials for your chosen database:
-
-
-
-```bash
-LETTA_API_KEY="..."
-CHROMA_API_KEY="..."
-CHROMA_TENANT="..."
-CHROMA_DATABASE="..."
-```
-
-
-```bash
-LETTA_API_KEY="..."
-MONGODB_URI="mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"
-MONGODB_DB_NAME="rag_demo"
-HF_API_KEY="..."
-```
-
-
-```bash
-LETTA_API_KEY="..."
-QDRANT_URL="https://xxxxx.cloud.qdrant.io"
-QDRANT_API_KEY="..."
-HF_API_KEY="..."
-```
-
-
-
-## Step 1: Set Up the Vector Database
-
-First, we need to populate your chosen vector database with the content of the research papers. We'll use two papers for this demo: ["Attention Is All You Need"](https://arxiv.org/abs/1706.03762) and ["BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding"](https://arxiv.org/abs/1810.04805).
-
-Before we begin, let's set up our development environment:
-
-
-```bash title="Python"
-# Create a Python virtual environment to keep dependencies isolated
-python -m venv venv
-source venv/bin/activate # On Windows, use: venv\Scripts\activate
-```
-
-```bash title="TypeScript"
-# Create a new Node.js project
-npm init -y
-
-# Create tsconfig.json for TypeScript configuration
-cat > tsconfig.json << 'EOF'
-{
- "compilerOptions": {
- "target": "ES2020",
- "module": "ESNext",
- "moduleResolution": "node",
- "esModuleInterop": true,
- "skipLibCheck": true,
- "strict": true
- }
-}
-EOF
-```
-
-
-Typescript users must update package.json to use ES modules:
-
-```typescript
-"type": "module"
-```
-
-Download the research papers using curl with the `-L` flag to follow redirects:
-
-```
-curl -L -o 1706.03762.pdf https://arxiv.org/pdf/1706.03762.pdf
-curl -L -o 1810.04805.pdf https://arxiv.org/pdf/1810.04805.pdf
-```
-
-Verify the PDFs downloaded correctly:
-
-```
-file 1706.03762.pdf 1810.04805.pdf
-```
-
-You should see output indicating these are PDF documents, not HTML files.
-
-Install the necessary packages for your chosen database:
-
-
-
-
-```txt title="Python"
-# requirements.txt
-letta-client
-chromadb
-pypdf
-python-dotenv
-```
-
-```bash title="TypeScript"
-npm install @letta-ai/letta-client dotenv
-npm install --save-dev typescript @types/node tsx
-```
-
-
-For Python, install with:
-```bash
-pip install -r requirements.txt
-```
-
-
-
-
-
-```txt title="Python"
-# requirements.txt
-letta-client
-pymongo
-pypdf
-python-dotenv
-requests
-certifi
-dnspython
-```
-
-```bash title="TypeScript"
-npm install @letta-ai/letta-client dotenv
-npm install --save-dev typescript @types/node tsx
-```
-
-
-For Python, install with:
-```bash
-pip install -r requirements.txt
-```
-
-
-
-
-
-```txt title="Python"
-# requirements.txt
-letta-client
-qdrant-client
-pypdf
-python-dotenv
-requests
-```
-
-```bash title="TypeScript"
-npm install @letta-ai/letta-client dotenv
-npm install --save-dev typescript @types/node tsx
-```
-
-
-For Python, install with:
-```bash
-pip install -r requirements.txt
-```
-
-
-
-
-Now create a `setup.py` or `setup.ts` file to load the PDFs, split them into chunks, and ingest them into your database:
-
-
-
-
-```python title="Python"
-import os
-import chromadb
-import pypdf
-from dotenv import load_dotenv
-
-load_dotenv()
-
-def main():
- # Connect to ChromaDB Cloud
- client = chromadb.CloudClient(
- tenant=os.getenv("CHROMA_TENANT"),
- database=os.getenv("CHROMA_DATABASE"),
- api_key=os.getenv("CHROMA_API_KEY")
- )
-
- # Create or get the collection
- collection = client.get_or_create_collection("rag_collection")
-
- # Ingest PDFs
- pdf_files = ["1706.03762.pdf", "1810.04805.pdf"]
- for pdf_file in pdf_files:
- print(f"Ingesting {pdf_file}...")
- reader = pypdf.PdfReader(pdf_file)
- for i, page in enumerate(reader.pages):
- collection.add(
- ids=[f"{pdf_file}-{i}"],
- documents=[page.extract_text()]
- )
-
- print("\nIngestion complete!")
- print(f"Total documents in collection: {collection.count()}")
-
-if __name__ == "__main__":
- main()
-```
-
-```typescript title="TypeScript"
-import { CloudClient } from 'chromadb';
-import { DefaultEmbeddingFunction } from '@chroma-core/default-embed';
-import * as dotenv from 'dotenv';
-import * as path from 'path';
-import * as fs from 'fs';
-import { pdfToPages } from 'pdf-ts';
-
-dotenv.config();
-
-async function main() {
- // Connect to ChromaDB Cloud
- const client = new CloudClient({
- apiKey: process.env.CHROMA_API_KEY || '',
- tenant: process.env.CHROMA_TENANT || '',
- database: process.env.CHROMA_DATABASE || ''
- });
-
- // Create embedding function
- const embedder = new DefaultEmbeddingFunction();
-
- // Create or get the collection
- const collection = await client.getOrCreateCollection({
- name: 'rag_collection',
- embeddingFunction: embedder
- });
-
- // Ingest PDFs
- const pdfFiles = ['1706.03762.pdf', '1810.04805.pdf'];
-
- for (const pdfFile of pdfFiles) {
- console.log(`Ingesting ${pdfFile}...`);
- const pdfPath = path.join(__dirname, pdfFile);
- const dataBuffer = fs.readFileSync(pdfPath);
-
- const pages = await pdfToPages(dataBuffer);
-
- for (let i = 0; i < pages.length; i++) {
- const text = pages[i].text.trim();
- if (text) {
- await collection.add({
- ids: [`${pdfFile}-${i}`],
- documents: [text]
- });
- }
- }
- }
-
- console.log('\nIngestion complete!');
- const count = await collection.count();
- console.log(`Total documents in collection: ${count}`);
-}
-
-main().catch(console.error);
-```
-
-
-
-
-
-```python title="Python"
-import os
-import pymongo
-import pypdf
-import requests
-import certifi
-from dotenv import load_dotenv
-
-load_dotenv()
-
-def get_embedding(text, api_key):
- """Get embedding from Hugging Face Inference API"""
- API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5"
- headers = {"Authorization": f"Bearer {api_key}"}
-
- response = requests.post(API_URL, headers=headers, json={"inputs": [text], "options": {"wait_for_model": True}})
-
- if response.status_code == 200:
- return response.json()[0]
- else:
- raise Exception(f"HF API error: {response.status_code} - {response.text}")
-
-def main():
- hf_api_key = os.getenv("HF_API_KEY")
- mongodb_uri = os.getenv("MONGODB_URI")
- db_name = os.getenv("MONGODB_DB_NAME")
-
- if not all([hf_api_key, mongodb_uri, db_name]):
- print("Error: Ensure HF_API_KEY, MONGODB_URI, and MONGODB_DB_NAME are in .env file")
- return
-
- # Connect to MongoDB Atlas using certifi
- client = pymongo.MongoClient(mongodb_uri, tlsCAFile=certifi.where())
- db = client[db_name]
- collection = db["rag_collection"]
-
- # Ingest PDFs
- pdf_files = ["1706.03762.pdf", "1810.04805.pdf"]
- for pdf_file in pdf_files:
- print(f"Ingesting {pdf_file}...")
- reader = pypdf.PdfReader(pdf_file)
- for i, page in enumerate(reader.pages):
- text = page.extract_text()
- if not text: # Skip empty pages
- continue
-
- # Generate embedding using Hugging Face
- print(f" Processing page {i+1}...")
- try:
- embedding = get_embedding(text, hf_api_key)
- collection.insert_one({
- "_id": f"{pdf_file}-{i}",
- "text": text,
- "embedding": embedding,
- "source": pdf_file,
- "page": i
- })
- except Exception as e:
- print(f" Could not process page {i+1}: {e}")
-
-
- print("\nIngestion complete!")
- print(f"Total documents in collection: {collection.count_documents({})}")
-
- # Create vector search index
- print("\nNext: Go to your MongoDB Atlas dashboard and create a search index named 'vector_index'")
- print("Use the following JSON definition:")
- print('''{
- "fields": [
- {
- "type": "vector",
- "path": "embedding",
- "numDimensions": 384,
- "similarity": "cosine"
- }
- ]
-}''')
-
-if __name__ == "__main__":
- main()
-```
-
-```typescript title="TypeScript"
-import { MongoClient } from 'mongodb';
-import * as dotenv from 'dotenv';
-import { pdfToPages } from 'pdf-ts';
-import * as fs from 'fs';
-import fetch from 'node-fetch';
-
-dotenv.config();
-
-async function getEmbedding(text: string, apiKey: string): Promise {
- const API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5";
- const headers = {
- "Authorization": `Bearer ${apiKey}`,
- "Content-Type": "application/json"
- };
-
- const response = await fetch(API_URL, {
- method: 'POST',
- headers: headers,
- body: JSON.stringify({
- inputs: [text],
- options: { wait_for_model: true }
- })
- });
-
- if (response.ok) {
- const result: any = await response.json();
- return result[0];
- } else {
- const errorText = await response.text();
- throw new Error(`HF API error: ${response.status} - ${errorText}`);
- }
-}
-
-async function main() {
- const hfApiKey = process.env.HF_API_KEY || '';
- const mongoUri = process.env.MONGODB_URI || '';
- const dbName = process.env.MONGODB_DB_NAME || '';
-
- if (!hfApiKey || !mongoUri || !dbName) {
- console.error('Error: Ensure HF_API_KEY, MONGODB_URI, and MONGODB_DB_NAME are in .env file');
- return;
- }
-
- // Connect to MongoDB Atlas
- const client = new MongoClient(mongoUri);
-
- try {
- await client.connect();
- console.log('Connected to MongoDB Atlas');
-
- const db = client.db(dbName);
- const collection = db.collection('rag_collection');
-
- // Ingest PDFs
- const pdfFiles = ['1706.03762.pdf', '1810.04805.pdf'];
-
- for (const pdfFile of pdfFiles) {
- console.log(`Ingesting ${pdfFile}...`);
-
- const dataBuffer = fs.readFileSync(pdfFile);
- const pages = await pdfToPages(dataBuffer);
-
- for (let i = 0; i < pages.length; i++) {
- const text = pages[i].text;
-
- if (!text || text.trim().length === 0) {
- continue; // Skip empty pages
- }
-
- // Generate embedding using Hugging Face
- console.log(` Processing page ${i + 1}...`);
- try {
- const embedding = await getEmbedding(text, hfApiKey);
-
- await collection.insertOne({
- _id: `${pdfFile}-${i}`,
- text: text,
- embedding: embedding,
- source: pdfFile,
- page: i
- });
- } catch (error) {
- console.log(` Could not process page ${i + 1}: ${error}`);
- }
- }
- }
-
- const docCount = await collection.countDocuments({});
- console.log('\nIngestion complete!');
- console.log(`Total documents in collection: ${docCount}`);
-
- console.log('\nNext: Go to your MongoDB Atlas dashboard and create a search index named "vector_index"');
- console.log(JSON.stringify({
- "fields": [
- {
- "type": "vector",
- "path": "embedding",
- "numDimensions": 384,
- "similarity": "cosine"
- }
- ]
- }, null, 2));
-
- } catch (error) {
- console.error('Error:', error);
- } finally {
- await client.close();
- }
-}
-
-main();
-```
-
-
-
-
-
-```python title="Python"
-import os
-import pypdf
-import requests
-from dotenv import load_dotenv
-from qdrant_client import QdrantClient
-from qdrant_client.models import Distance, VectorParams, PointStruct
-
-load_dotenv()
-
-def get_embedding(text, api_key):
- """Get embedding from Hugging Face Inference API"""
- API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5"
- headers = {"Authorization": f"Bearer {api_key}"}
-
- response = requests.post(API_URL, headers=headers, json={"inputs": text, "options": {"wait_for_model": True}})
-
- if response.status_code == 200:
- return response.json()
- else:
- raise Exception(f"HF API error: {response.status_code} - {response.text}")
-
-def main():
- hf_api_key = os.getenv("HF_API_KEY")
-
- if not hf_api_key:
- print("Error: HF_API_KEY not found in .env file")
- return
-
- # Connect to Qdrant Cloud
- client = QdrantClient(
- url=os.getenv("QDRANT_URL"),
- api_key=os.getenv("QDRANT_API_KEY")
- )
-
- # Create collection
- collection_name = "rag_collection"
-
- # Check if collection exists, if not create it
- collections = client.get_collections().collections
- if collection_name not in [c.name for c in collections]:
- client.create_collection(
- collection_name=collection_name,
- vectors_config=VectorParams(size=384, distance=Distance.COSINE)
- )
-
- # Ingest PDFs
- pdf_files = ["1706.03762.pdf", "1810.04805.pdf"]
- point_id = 0
-
- for pdf_file in pdf_files:
- print(f"Ingesting {pdf_file}...")
- reader = pypdf.PdfReader(pdf_file)
- for i, page in enumerate(reader.pages):
- text = page.extract_text()
-
- # Generate embedding using Hugging Face
- print(f" Processing page {i+1}...")
- embedding = get_embedding(text, hf_api_key)
-
- client.upsert(
- collection_name=collection_name,
- points=[
- PointStruct(
- id=point_id,
- vector=embedding,
- payload={"text": text, "source": pdf_file, "page": i}
- )
- ]
- )
- point_id += 1
-
- print("\nIngestion complete!")
- collection_info = client.get_collection(collection_name)
- print(f"Total documents in collection: {collection_info.points_count}")
-
-if __name__ == "__main__":
- main()
-```
-
-```typescript title="TypeScript"
-import { QdrantClient } from '@qdrant/js-client-rest';
-import { pdfToPages } from 'pdf-ts';
-import dotenv from 'dotenv';
-import fetch from 'node-fetch';
-import * as fs from 'fs';
-
-dotenv.config();
-
-async function getEmbedding(text: string, apiKey: string): Promise {
- const API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5";
-
- const response = await fetch(API_URL, {
- method: 'POST',
- headers: {
- "Authorization": `Bearer ${apiKey}`,
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- inputs: [text],
- options: { wait_for_model: true }
- })
- });
-
- if (response.ok) {
- const result: any = await response.json();
- return result[0];
- } else {
- const error = await response.text();
- throw new Error(`HuggingFace API error: ${response.status} - ${error}`);
- }
-}
-
-async function main() {
- const hfApiKey = process.env.HF_API_KEY || '';
-
- if (!hfApiKey) {
- console.error('Error: HF_API_KEY not found in .env file');
- return;
- }
-
- // Connect to Qdrant Cloud
- const client = new QdrantClient({
- url: process.env.QDRANT_URL || '',
- apiKey: process.env.QDRANT_API_KEY || ''
- });
-
- const collectionName = 'rag_collection';
-
- // Check if collection exists, if not create it
- const collections = await client.getCollections();
- const collectionExists = collections.collections.some(c => c.name === collectionName);
-
- if (!collectionExists) {
- console.log('Creating collection...');
- await client.createCollection(collectionName, {
- vectors: {
- size: 384,
- distance: 'Cosine'
- }
- });
- }
-
- // Ingest PDFs
- const pdfFiles = ['1706.03762.pdf', '1810.04805.pdf'];
- let pointId = 0;
-
- for (const pdfFile of pdfFiles) {
- console.log(`\nIngesting ${pdfFile}...`);
- const dataBuffer = fs.readFileSync(pdfFile);
- const pages = await pdfToPages(dataBuffer);
-
- for (let i = 0; i < pages.length; i++) {
- const text = pages[i].text;
-
- console.log(` Processing page ${i + 1}...`);
- const embedding = await getEmbedding(text, hfApiKey);
-
- await client.upsert(collectionName, {
- wait: true,
- points: [
- {
- id: pointId,
- vector: embedding,
- payload: {
- text: text,
- source: pdfFile,
- page: i
- }
- }
- ]
- });
- pointId++;
- }
- }
-
- console.log('\nIngestion complete!');
- const collectionInfo = await client.getCollection(collectionName);
- console.log(`Total documents in collection: ${collectionInfo.points_count}`);
-}
-
-main().catch(console.error);
-```
-
-
-
-
-Run the script from your terminal:
-
-
-```bash title="Python"
-python setup.py
-```
-
-```bash title="TypeScript"
-npx tsx setup.ts
-```
-
-
-If you are using MongoDB Atlas, you must manually create a vector search index by following the steps below.
-
-
-
-**MongoDB Atlas users:** The setup script ingests your data, but MongoDB Atlas requires you to manually create a vector search index before queries will work. Follow these steps carefully.
-
-
-
-
- Log in to your [MongoDB Atlas dashboard](https://cloud.mongodb.com/), navigate to your cluster, and click on the **"Atlas Search"** tab.
-
-
- Click **"Create Search Index"**, then choose **"JSON Editor"** (not "Visual Editor").
-
-
- - Database: Select **`rag_demo`** (or whatever you set as `MONGODB_DB_NAME`)
- - Collection: Select **`rag_collection`**
-
-
- - Index Name: Enter **`vector_index`** (this exact name is required by the code)
- - Paste this JSON definition:
- ```json
- {
- "fields": [
- {
- "type": "vector",
- "path": "embedding",
- "numDimensions": 384,
- "similarity": "cosine"
- }
- ]
- }
- ```
- **Note:** 384 dimensions is for Hugging Face's `BAAI/bge-small-en-v1.5` model.
-
-
- Click **"Create Search Index"**. The index will take a few minutes to build. Wait until the status shows as **"Active"** before proceeding.
-
-
-
-
-Your vector database is now populated with research paper content and ready to query.
-
-## Step 2: Create a Custom Search Tool
-
-A Letta tool is a Python function that your agent can call. We'll create a function that searches your vector database and returns the results. Letta handles the complexities of exposing this function to the agent securely.
-
-
-**TypeScript users:** Letta tools execute in Python, even when called from TypeScript. Create a `tools.ts` file that exports the Python code as a string constant, which you'll use in Step 3 to create the tool.
-
-
-Create a new file named `tools.py` (Python) or `tools.ts` (TypeScript) with the appropriate implementation for your database:
-
-
-
-
-```python title="Python"
-def search_research_papers(query_text: str, n_results: int = 1) -> str:
- """
- Searches the research paper collection for a given query.
-
- Args:
- query_text (str): The text to search for.
- n_results (int): The number of results to return.
-
- Returns:
- str: The most relevant document found.
- """
- import chromadb
- import os
-
- # ChromaDB Cloud Client
- # This tool code is executed on the Letta server. It expects the ChromaDB
- # credentials to be passed as environment variables.
- api_key = os.getenv("CHROMA_API_KEY")
- tenant = os.getenv("CHROMA_TENANT")
- database = os.getenv("CHROMA_DATABASE")
-
- if not all([api_key, tenant, database]):
- raise ValueError("CHROMA_API_KEY, CHROMA_TENANT, and CHROMA_DATABASE must be set as environment variables.")
-
- client = chromadb.CloudClient(
- tenant=tenant,
- database=database,
- api_key=api_key
- )
-
- collection = client.get_or_create_collection("rag_collection")
-
- try:
- results = collection.query(
- query_texts=[query_text],
- n_results=n_results
- )
-
- document = results['documents'][0][0]
- return document
- except Exception as e:
- return f"Tool failed with error: {e}"
-```
-
-```typescript title="TypeScript"
-/**
- * This file contains the Python tool code as a string.
- * Letta tools execute in Python, so we define the Python source code here.
- */
-
-export const searchResearchPapersToolCode = `def search_research_papers(query_text: str, n_results: int = 1) -> str:
- """
- Searches the research paper collection for a given query.
-
- Args:
- query_text (str): The text to search for.
- n_results (int): The number of results to return.
-
- Returns:
- str: The most relevant document found.
- """
- import chromadb
- import os
-
- # ChromaDB Cloud Client
- # This tool code is executed on the Letta server. It expects the ChromaDB
- # credentials to be passed as environment variables.
- api_key = os.getenv("CHROMA_API_KEY")
- tenant = os.getenv("CHROMA_TENANT")
- database = os.getenv("CHROMA_DATABASE")
-
- if not all([api_key, tenant, database]):
- raise ValueError("CHROMA_API_KEY, CHROMA_TENANT, and CHROMA_DATABASE must be set as environment variables.")
-
- client = chromadb.CloudClient(
- tenant=tenant,
- database=database,
- api_key=api_key
- )
-
- collection = client.get_or_create_collection("rag_collection")
-
- try:
- results = collection.query(
- query_texts=[query_text],
- n_results=n_results
- )
-
- document = results['documents'][0][0]
- return document
- except Exception as e:
- return f"Tool failed with error: {e}"
-`;
-```
-
-
-
-
-
-```python title="Python"
-import os
-
-def search_research_papers(query_text: str, n_results: int = 1) -> str:
- """
- Searches the research paper collection for a given query using Hugging Face embeddings.
-
- Args:
- query_text (str): The text to search for.
- n_results (int): The number of results to return.
-
- Returns:
- str: The most relevant documents found.
- """
- import requests
- import pymongo
- import certifi
-
- try:
- n_results = int(n_results)
- except (ValueError, TypeError):
- n_results = 1
-
- mongodb_uri = os.getenv("MONGODB_URI")
- db_name = os.getenv("MONGODB_DB_NAME")
- hf_api_key = os.getenv("HF_API_KEY")
-
- if not all([mongodb_uri, db_name, hf_api_key]):
- raise ValueError("MONGODB_URI, MONGODB_DB_NAME, and HF_API_KEY must be set as environment variables.")
-
- # --- Hugging Face API Call ---
- try:
- response = requests.post(
- "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5",
- headers={"Authorization": f"Bearer {hf_api_key}"},
- json={"inputs": [query_text], "options": {"wait_for_model": True}},
- timeout=30
- )
- response.raise_for_status()
- query_embedding = response.json()[0]
- except requests.exceptions.RequestException as e:
- return f"Hugging Face API request failed: {e}"
-
- # --- MongoDB Atlas Connection & Search ---
- try:
- client = pymongo.MongoClient(mongodb_uri, tlsCAFile=certifi.where(), serverSelectionTimeoutMS=30000)
- collection = client[db_name]["rag_collection"]
- pipeline = [
- {
- "$vectorSearch": {
- "index": "vector_index",
- "path": "embedding",
- "queryVector": query_embedding,
- "numCandidates": 100,
- "limit": n_results
- }
- },
- {
- "$project": {
- "text": 1,
- "source": 1,
- "page": 1,
- "score": {"$meta": "vectorSearchScore"}
- }
- }
- ]
- results = list(collection.aggregate(pipeline))
- except pymongo.errors.PyMongoError as e:
- return f"MongoDB operation failed: {e}"
-
- # --- Final Processing ---
- documents = [doc.get("text", "") for doc in results]
- return "\n\n".join(documents) if documents else "No results found."
-```
-
-```typescript title="TypeScript"
-/**
- * This file contains the Python tool code as a string.
- * Letta tools execute in Python, so we define the Python source code here.
- */
-
-export const searchResearchPapersToolCode = `import os
-
-def search_research_papers(query_text: str, n_results: int = 1) -> str:
- """
- Searches the research paper collection for a given query using Hugging Face embeddings.
-
- Args:
- query_text (str): The text to search for.
- n_results (int): The number of results to return.
-
- Returns:
- str: The most relevant documents found.
- """
- import requests
- import pymongo
- import certifi
-
- try:
- n_results = int(n_results)
- except (ValueError, TypeError):
- n_results = 1
-
- mongodb_uri = os.getenv("MONGODB_URI")
- db_name = os.getenv("MONGODB_DB_NAME")
- hf_api_key = os.getenv("HF_API_KEY")
-
- if not all([mongodb_uri, db_name, hf_api_key]):
- raise ValueError("MONGODB_URI, MONGODB_DB_NAME, and HF_API_KEY must be set as environment variables.")
-
- # --- Hugging Face API Call ---
- try:
- response = requests.post(
- "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5",
- headers={"Authorization": f"Bearer {hf_api_key}"},
- json={"inputs": [query_text], "options": {"wait_for_model": True}},
- timeout=30
- )
- response.raise_for_status()
- query_embedding = response.json()[0]
- except requests.exceptions.RequestException as e:
- return f"Hugging Face API request failed: {e}"
-
- # --- MongoDB Atlas Connection & Search ---
- try:
- client = pymongo.MongoClient(mongodb_uri, tlsCAFile=certifi.where(), serverSelectionTimeoutMS=30000)
- collection = client[db_name]["rag_collection"]
- pipeline = [
- {
- "$vectorSearch": {
- "index": "vector_index",
- "path": "embedding",
- "queryVector": query_embedding,
- "numCandidates": 100,
- "limit": n_results
- }
- },
- {
- "$project": {
- "text": 1,
- "source": 1,
- "page": 1,
- "score": {"$meta": "vectorSearchScore"}
- }
- }
- ]
- results = list(collection.aggregate(pipeline))
- except pymongo.errors.PyMongoError as e:
- return f"MongoDB operation failed: {e}"
-
- # --- Final Processing ---
- documents = [doc.get("text", "") for doc in results]
- return "\\n\\n".join(documents) if documents else "No results found."
-`;
-```
-
-
-
-
-
-```python title="Python"
-def search_research_papers(query_text: str, n_results: int = 1) -> str:
- """
- Searches the research paper collection for a given query using Hugging Face embeddings.
-
- Args:
- query_text (str): The text to search for.
- n_results (int): The number of results to return.
-
- Returns:
- str: The most relevant documents found.
- """
- import os
- import requests
- from qdrant_client import QdrantClient
-
- # Qdrant Cloud Client
- url = os.getenv("QDRANT_URL")
- api_key = os.getenv("QDRANT_API_KEY")
- hf_api_key = os.getenv("HF_API_KEY")
-
- if not all([url, api_key, hf_api_key]):
- raise ValueError("QDRANT_URL, QDRANT_API_KEY, and HF_API_KEY must be set as environment variables.")
-
- # Connect to Qdrant
- client = QdrantClient(url=url, api_key=api_key)
-
- try:
- # Generate embedding using Hugging Face
- API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5"
- headers = {"Authorization": f"Bearer {hf_api_key}"}
- response = requests.post(API_URL, headers=headers, json={"inputs": query_text, "options": {"wait_for_model": True}})
-
- if response.status_code != 200:
- return f"HF API error: {response.status_code}"
-
- query_embedding = response.json()
-
- # Search Qdrant
- results = client.query_points(
- collection_name="rag_collection",
- query=query_embedding,
- limit=n_results
- )
-
- documents = [hit.payload["text"] for hit in results.points]
- return "\n\n".join(documents) if documents else "No results found."
- except Exception as e:
- return f"Tool failed with error: {e}"
-```
-
-```typescript title="TypeScript"
-/**
- * This file contains the Python tool code as a string.
- * Letta tools execute in Python, so we define the Python source code here.
- */
-
-export const searchResearchPapersToolCode = `def search_research_papers(query_text: str, n_results: int = 1) -> str:
- """
- Searches the research paper collection for a given query using Hugging Face embeddings.
-
- Args:
- query_text (str): The text to search for.
- n_results (int): The number of results to return.
-
- Returns:
- str: The most relevant documents found.
- """
- import os
- import requests
- from qdrant_client import QdrantClient
-
- # Qdrant Cloud Client
- url = os.getenv("QDRANT_URL")
- api_key = os.getenv("QDRANT_API_KEY")
- hf_api_key = os.getenv("HF_API_KEY")
-
- if not all([url, api_key, hf_api_key]):
- raise ValueError("QDRANT_URL, QDRANT_API_KEY, and HF_API_KEY must be set as environment variables.")
-
- # Connect to Qdrant
- client = QdrantClient(url=url, api_key=api_key)
-
- try:
- # Generate embedding using Hugging Face
- API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5"
- headers = {"Authorization": f"Bearer {hf_api_key}"}
- response = requests.post(API_URL, headers=headers, json={"inputs": query_text, "options": {"wait_for_model": True}})
-
- if response.status_code != 200:
- return f"HF API error: {response.status_code}"
-
- query_embedding = response.json()
-
- # Search Qdrant
- results = client.query_points(
- collection_name="rag_collection",
- query=query_embedding,
- limit=n_results
- )
-
- documents = [hit.payload["text"] for hit in results.points]
- return "\\n\\n".join(documents) if documents else "No results found."
- except Exception as e:
- return f"Tool failed with error: {e}"
-`;
-```
-
-
-
-
-This function takes a query, connects to your database, retrieves the most relevant documents, and returns them as a single string.
-
-## Step 3: Configure an Agentic Research Assistant
-
-Next, we'll create a new agent. This agent will have a specific persona that instructs it on how to behave and, most importantly, it will be equipped with our new search tool.
-
-Create a file named `create_agentic_agent.py` (Python) or `create_agentic_agent.ts` (TypeScript):
-
-
-```python title="Python"
-import os
-from letta_client import Letta
-from dotenv import load_dotenv
-from tools import search_research_papers
-
-load_dotenv()
-
-# Initialize the Letta client
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# Create a tool from our Python function
-search_tool = client.tools.create_from_function(func=search_research_papers)
-
-# Define the agent's persona
-persona = """You are a world-class research assistant. Your goal is to answer questions accurately by searching through a database of research papers. When a user asks a question, first use the `search_research_papers` tool to find relevant information. Then, answer the user's question based on the information returned by the tool."""
-
-# Create the agent with the tool attached
-agent = client.agents.create(
- name="Agentic RAG Assistant",
- description="A smart agent that can search a vector database to answer questions.",
- memory_blocks=[
- {
- "label": "persona",
- "value": persona
- }
- ],
- tools=[search_tool.name]
-)
-
-print(f"Agent '{agent.name}' created with ID: {agent.id}")
-```
-
-```typescript title="TypeScript"
-import { LettaClient } from '@letta-ai/letta-client';
-import * as dotenv from 'dotenv';
-import { searchResearchPapersToolCode } from './tools.js';
-
-dotenv.config();
-
-async function main() {
- // Initialize the Letta client
- const client = new LettaClient({
- token: process.env.LETTA_API_KEY || ''
- });
-
- // Create the tool from the Python code imported from tools.ts
- const searchTool = await client.tools.create({
- sourceCode: searchResearchPapersToolCode,
- sourceType: 'python'
- });
-
- console.log(`Tool '${searchTool.name}' created with ID: ${searchTool.id}`);
-
- // Define the agent's persona
- const persona = `You are a world-class research assistant. Your goal is to answer questions accurately by searching through a database of research papers. When a user asks a question, first use the \`search_research_papers\` tool to find relevant information. Then, answer the user's question based on the information returned by the tool.`;
-
- // Create the agent with the tool attached
- const agent = await client.agents.create({
- name: 'Agentic RAG Assistant',
- description: 'A smart agent that can search a vector database to answer questions.',
- memoryBlocks: [
- {
- label: 'persona',
- value: persona
- }
- ],
- toolIds: [searchTool.id]
- });
-
- console.log(`Agent '${agent.name}' created with ID: ${agent.id}`);
-}
-
-main().catch(console.error);
-```
-
-
-
-**TypeScript users:** Notice how the TypeScript version imports `searchResearchPapersToolCode` from `tools.ts` (the file you created in Step 2). This keeps the code organized, just like the Python version imports from `tools.py`.
-
-
-
-Run this script once to create the agent in your Letta project:
-
-
-```bash title="Python"
-python create_agentic_agent.py
-```
-
-```bash title="TypeScript"
-npx tsx create_agentic_agent.ts
-```
-
-
-### Configure Tool Dependencies and Environment Variables
-
-For the tool to work within Letta's environment, we need to configure its dependencies and environment variables through the Letta dashboard.
-
-
-
- Navigate to your Letta dashboard and find the "Agentic RAG Assistant" agent you just created.
-
-
- Click on your agent to open the Agent Development Environment (ADE).
-
-
- In the ADE, select **Tools** from the sidebar, find and click on the `search_research_papers` tool, then click on the **Dependencies** tab.
-
- Add the following dependencies based on your database:
-
-
-
- ```txt
- chromadb
- ```
-
-
-
- ```txt
- pymongo
- requests
- certifi
- dnspython
- ```
-
-
-
- ```txt
- qdrant-client
- requests
- ```
-
-
-
- 
-
-
- In the same tool configuration, navigate to **Simulator** > **Environment**.
-
- Add the following environment variables with their corresponding values from your `.env` file:
-
-
-
- ```txt
- CHROMA_API_KEY
- CHROMA_TENANT
- CHROMA_DATABASE
- ```
-
-
-
- ```txt
- MONGODB_URI
- MONGODB_DB_NAME
- HF_API_KEY
- ```
-
-
-
- ```txt
- QDRANT_URL
- QDRANT_API_KEY
- HF_API_KEY
- ```
-
-
-
- Make sure to click upload button next to the environment variable to update the agent with the variable.
-
- 
-
-
-
-Now, when the agent calls this tool, Letta's execution environment will know to install the necessary dependencies and will have access to the necessary credentials to connect to your database.
-
-## Step 4: Let the Agent Lead the Conversation
-
-With the agentic setup, our client-side code becomes incredibly simple. We no longer need to worry about retrieving context, we just send the user's raw question to the agent and let it handle the rest.
-
-Create the `agentic_rag.py` or `agentic_rag.ts` script:
-
-
-```python title="Python"
-import os
-from letta_client import Letta
-from dotenv import load_dotenv
-
-load_dotenv()
-
-# Initialize client
-letta_client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-AGENT_ID = "your-agentic-agent-id" # Replace with your new agent ID
-
-def main():
- while True:
- user_query = input("\nAsk a question about the research papers: ")
- if user_query.lower() in ['exit', 'quit']:
- break
-
- response = letta_client.agents.messages.create(
- agent_id=AGENT_ID,
- messages=[{"role": "user", "content": user_query}]
- )
-
- for message in response.messages:
- if message.message_type == 'assistant_message':
- print(f"\nAgent: {message.content}")
-
-if __name__ == "__main__":
- main()
-```
-
-```typescript title="TypeScript"
-import { LettaClient } from '@letta-ai/letta-client';
-import * as dotenv from 'dotenv';
-import * as readline from 'readline';
-
-dotenv.config();
-
-const AGENT_ID = 'your-agentic-agent-id'; // Replace with your new agent ID
-
-async function main() {
- // Initialize client
- const lettaClient = new LettaClient({
- token: process.env.LETTA_API_KEY || ''
- });
-
- const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout
- });
-
- const askQuestion = (query: string): Promise => {
- return new Promise((resolve) => {
- rl.question(query, resolve);
- });
- };
-
- while (true) {
- const userQuery = await askQuestion('\nAsk a question about the research papers (or type "exit" to quit): ');
-
- if (userQuery.toLowerCase() === 'exit' || userQuery.toLowerCase() === 'quit') {
- rl.close();
- break;
- }
-
- const response = await lettaClient.agents.messages.create(AGENT_ID, {
- messages: [{ role: 'user', content: userQuery }]
- });
-
- for (const message of response.messages) {
- if (message.messageType === 'assistant_message') {
- console.log(`\nAgent: ${(message as any).content}`);
- }
- }
- }
-}
-
-main().catch(console.error);
-```
-
-
-
-Replace `your-agentic-agent-id` with the ID of the new agent you just created.
-
-
-When you run this script, the agent receives the question, understands from its persona that it needs to search for information, calls the `search_research_papers` tool, gets the context, and then formulates an answer. All the RAG logic is handled by the agent, not your application.
-
-## Next Steps
-
-Now that you've integrated Agentic RAG with Letta, you can expand on this foundation:
-
-
-
- Learn how to manage retrieval on the client-side for complete control.
-
-
- Explore creating more advanced custom tools for your agents.
-
-
diff --git a/fern/pages/cookbooks/rag-overview.mdx b/fern/pages/cookbooks/rag-overview.mdx
deleted file mode 100644
index 86944606..00000000
--- a/fern/pages/cookbooks/rag-overview.mdx
+++ /dev/null
@@ -1,59 +0,0 @@
----
-title: RAG with Letta
-subtitle: Connect your custom RAG pipeline to Letta agents
-slug: guides/rag/overview
----
-
-If you have an existing Retrieval-Augmented Generation (RAG) pipeline, you can connect it to your Letta agents. While Letta provides built-in features like archival memory, you can integrate your own RAG pipeline just as you would with any LLM API. This gives you full control over your data and retrieval methods.
-
-## What is RAG?
-
-Retrieval-Augmented Generation (RAG) enhances LLM responses by retrieving relevant information from external data sources before generating an answer. Instead of relying on the model's training data, a RAG system:
-
-1. Takes a user query.
-2. Searches a vector database for relevant documents.
-3. Includes those documents in the LLM's context.
-4. Generates an informed response based on the retrieved information.
-
-## Choosing Your RAG Approach
-
-Letta supports two approaches for integrating RAG, depending on how much control you want over the retrieval process.
-
-| Aspect | Simple RAG | Agentic RAG |
-|--------|------------|-------------|
-| **Who Controls Retrieval** | Your application controls when retrieval happens and what the retrieval query is. | The agent decides when to retrieve and what query to use. |
-| **Context Inclusion** | You can always include retrieval results in the context. | Retrieval happens only when the agent determines it's needed. |
-| **Latency** | Lower – typically single-hop, as the agent doesn't need to do a tool call. | Higher – requires tool calls for retrieval. |
-| **Client Code** | More complex, as it handles retrieval logic. | Simpler, as it just sends the user query. |
-| **Customization** | You have full control via your retrieval function. | You have full control via your custom tool definition. |
-
-Both approaches work with any vector database. Our tutorials include examples for **ChromaDB**, **MongoDB Atlas**, and **Qdrant**.
-
-## Next Steps
-
-Ready to integrate RAG with your Letta agents?
-
-
-
- Learn how to manage retrieval on the client-side and inject context directly into your agent's messages.
-
-
- Learn how to empower your agent with custom search tools for autonomous retrieval.
-
-
-
-## Additional Resources
-
-- [Custom Tools](/guides/agents/custom-tools) - Learn more about creating custom tools for your agents.
-- [Memory Management](/guides/agents/memory) - Understand how Letta's built-in memory works.
-- [Agent Development Environment](/guides/ade) - Configure and test your agents in the web interface.
diff --git a/fern/pages/cookbooks/rag-simple.mdx b/fern/pages/cookbooks/rag-simple.mdx
deleted file mode 100644
index daeaf751..00000000
--- a/fern/pages/cookbooks/rag-simple.mdx
+++ /dev/null
@@ -1,1511 +0,0 @@
----
-title: Simple RAG with Letta
-subtitle: Manage retrieval on the client-side and inject context into your agent
-slug: guides/rag/simple
----
-
-In the Simple RAG approach, your application manages the retrieval process. You query your vector database, retrieve the relevant documents, and include them directly in the message you send to your Letta agent.
-
-By the end of this tutorial, you'll have a research assistant that uses your vector database to answer questions about scientific papers.
-
-## Prerequisites
-
-To follow along, you need free accounts for:
-
-- **[Letta](https://www.letta.com)** - To access the agent development platform
-- **[Hugging Face](https://huggingface.co/)** - For generating embeddings (MongoDB and Qdrant users only)
-- **One of the following vector databases:**
- - **[ChromaDB Cloud](https://www.trychroma.com/)** for a hosted vector database
- - **[MongoDB Atlas](https://www.mongodb.com/cloud/atlas/register)** for vector search with MongoDB
- - **[Qdrant Cloud](https://cloud.qdrant.io/)** for a high-performance vector database
-
-You will also need Python 3.8+ or Node.js v18+ and a code editor.
-
-
-**MongoDB and Qdrant users:** This guide uses Hugging Face's Inference API for generating embeddings. This approach keeps the tool code lightweight enough to run in Letta's sandbox environment.
-
-
-## Getting Your API Keys
-
-We'll need API keys for Letta and your chosen vector database.
-
-
-
-
-
- If you don't have one, sign up for a free account at [letta.com](https://www.letta.com).
-
-
- Once logged in, click on **API keys** in the sidebar.
- 
-
-
- Click **+ Create API key**, give it a descriptive name, and click **Confirm**. Copy the key and save it somewhere safe.
-
-
-
-
-
-
-
- Sign up for a free account on the [ChromaDB Cloud website](https://www.trychroma.com/).
-
-
- From your dashboard, create a new database.
- 
-
-
- In your project settings, you'll find your **API Key**, **Tenant**, **Database**, and **Host URL**. We'll need all of these for our scripts.
- 
-
-
-
-
-
-
-
- Sign up for a free account at [mongodb.com/cloud/atlas/register](https://www.mongodb.com/cloud/atlas/register).
-
-
- Click **Build a Cluster** and select the free tier (M0). Choose your preferred cloud provider and region and click **Create deployment**.
- 
-
-
- Next, set up connection security.
- 1. Create a database user, then click **Choose a connection method**
- 2. Choose **Drivers** to connect to your application, choose Python as the driver.
- 3. Copy the **entire** connection string, including the query parameters at the end. It will look like this:
-
- ```
- mongodb+srv://:@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
- ```
-
-
- Make sure to replace `` with your actual database user password. Keep all the query parameters (`?retryWrites=true&w=majority&appName=Cluster0`) they are required for proper connection configuration.
-
- 
-
-
- By default, MongoDB Atlas blocks all outside connections. You must grant access to the services that need to connect.
-
- 1. Navigate to **Database and Network Access** in the left sidebar.
- 2. Click **Add IP Address**.
- 3. For local development and testing, select **Allow Access From Anywhere**. This will add the IP address `0.0.0.0/0`.
- 4. Click **Confirm**.
-
- 
-
-
- For a production environment, you would replace `0.0.0.0/0` with a secure list of static IP addresses provided by your hosting service (e.g., Letta).
-
-
-
-
-
-
-
-
- Sign up for a free account at [cloud.qdrant.io](https://cloud.qdrant.io/).
-
-
- From your dashboard, click **Clusters** and then **+ Create**. Select the free tier and choose your preferred region.
-
- 
-
-
- Once your cluster is created, click on it to view details.
-
- Copy the following:
-
- 1. **API Key**
- 2. **Cluster URL**
-
- 
-
-
-
-
-
-
-
- Sign up for a free account at [huggingface.co](https://huggingface.co/join).
-
-
- Click the profile icon in the top right. Navigate to **Settings** > **Access Tokens** (or go directly to [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)).
-
-
- Click **New token**, give it a name (e.g., "Letta RAG Demo"), select **Read** role, and click **Create token**. Copy the token and save it securely.
- 
-
-
-
-
-The free tier includes 30,000 API requests per month, which is more than enough for development and testing.
-
-
-
-
-Once you have these credentials, create a `.env` file in your project directory. Add the credentials for your chosen database:
-
-
-
-```bash
-LETTA_API_KEY="..."
-CHROMA_API_KEY="..."
-CHROMA_TENANT="..."
-CHROMA_DATABASE="..."
-```
-
-
-```bash
-LETTA_API_KEY="..."
-MONGODB_URI="mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"
-MONGODB_DB_NAME="rag_demo"
-HF_API_KEY="..."
-```
-
-
-```bash
-LETTA_API_KEY="..."
-QDRANT_URL="https://xxxxx.cloud.qdrant.io"
-QDRANT_API_KEY="..."
-HF_API_KEY="..."
-```
-
-
-
-## Step 1: Set Up the Vector Database
-
-First, we need to populate your chosen vector database with the content of the research papers. We'll use two papers for this demo: ["Attention Is All You Need"](https://arxiv.org/abs/1706.03762) and ["BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding"](https://arxiv.org/abs/1810.04805).
-
-Before we begin, let's create a virtual environment to keep our dependencies isolated:
-
-
-
-Before we begin, let's create a Python virtual environment to keep our dependencies isolated:
-
-```bash
-python -m venv venv
-source venv/bin/activate # On Windows, use: venv\Scripts\activate
-```
-
-
-Before we begin, let's create a new Node.js project:
-
-```bash
-npm init -y
-```
-
-This will create a `package.json` file for you.
-
-Next, create a `tsconfig.json` file for TypeScript configuration:
-
-```json
-{
- "compilerOptions": {
- "target": "ES2020",
- "module": "ESNext",
- "moduleResolution": "node",
- "esModuleInterop": true,
- "skipLibCheck": true,
- "strict": true
- }
-}
-```
-
-Update your `package.json` to use ES modules by adding this line:
-
-```json
-"type": "module"
-```
-
-
-
-Download the research papers using curl with the `-L` flag to follow redirects:
-
-```
-curl -L -o 1706.03762.pdf https://arxiv.org/pdf/1706.03762.pdf
-curl -L -o 1810.04805.pdf https://arxiv.org/pdf/1810.04805.pdf
-```
-
-Verify the PDFs downloaded correctly:
-
-```
-file 1706.03762.pdf 1810.04805.pdf
-```
-
-You should see output indicating these are PDF documents, not HTML files.
-
-Install the necessary packages for your chosen database:
-
-
-
-
-```txt title="Python"
-# requirements.txt
-letta-client
-chromadb
-pypdf
-python-dotenv
-```
-
-```bash title="TypeScript"
-npm install @letta-ai/letta-client chromadb @chroma-core/default-embed dotenv pdf-ts
-npm install --save-dev typescript @types/node ts-node tsx
-```
-
-
-For Python, install with:
-```bash
-pip install -r requirements.txt
-```
-
-
-
-
-```txt title="Python"
-# requirements.txt
-letta-client
-pymongo
-pypdf
-python-dotenv
-requests
-certifi
-dnspython
-```
-
-```bash title="TypeScript"
-npm install @letta-ai/letta-client mongodb dotenv pdf-ts node-fetch
-npm install --save-dev typescript @types/node ts-node tsx
-```
-
-
-For Python, install with:
-```bash
-pip install -r requirements.txt
-```
-
-
-
-
-```txt title="Python"
-# requirements.txt
-letta-client
-qdrant-client
-pypdf
-python-dotenv
-requests
-```
-
-```bash title="TypeScript"
-npm install @letta-ai/letta-client @qdrant/js-client-rest dotenv node-fetch pdf-ts
-npm install --save-dev typescript @types/node ts-node tsx
-```
-
-
-For Python, install with:
-```bash
-pip install -r requirements.txt
-```
-
-
-
-Now create a `setup.py` or `setup.ts` file to load the PDFs, split them into chunks, and ingest them into your database:
-
-
-
-
-```python title="Python"
-import os
-import chromadb
-import pypdf
-from dotenv import load_dotenv
-
-load_dotenv()
-
-def main():
- # Connect to ChromaDB Cloud
- client = chromadb.CloudClient(
- tenant=os.getenv("CHROMA_TENANT"),
- database=os.getenv("CHROMA_DATABASE"),
- api_key=os.getenv("CHROMA_API_KEY")
- )
-
- # Create or get the collection
- collection = client.get_or_create_collection("rag_collection")
-
- # Ingest PDFs
- pdf_files = ["1706.03762.pdf", "1810.04805.pdf"]
- for pdf_file in pdf_files:
- print(f"Ingesting {pdf_file}...")
- reader = pypdf.PdfReader(pdf_file)
- for i, page in enumerate(reader.pages):
- text = page.extract_text()
- if text:
- collection.add(
- ids=[f"{pdf_file}-{i}"],
- documents=[text]
- )
-
- print("\nIngestion complete!")
- print(f"Total documents in collection: {collection.count()}")
-
-if __name__ == "__main__":
- main()
-```
-
-```typescript title="TypeScript"
-import { CloudClient } from 'chromadb';
-import { DefaultEmbeddingFunction } from '@chroma-core/default-embed';
-import * as dotenv from 'dotenv';
-import * as path from 'path';
-import * as fs from 'fs';
-import { pdfToPages } from 'pdf-ts';
-
-dotenv.config();
-
-async function main() {
- // Connect to ChromaDB Cloud
- const client = new CloudClient({
- apiKey: process.env.CHROMA_API_KEY || '',
- tenant: process.env.CHROMA_TENANT || '',
- database: process.env.CHROMA_DATABASE || ''
- });
-
- // Create embedding function
- const embedder = new DefaultEmbeddingFunction();
-
- // Create or get the collection
- const collection = await client.getOrCreateCollection({
- name: 'rag_collection',
- embeddingFunction: embedder
- });
-
- // Ingest PDFs
- const pdfFiles = ['1706.03762.pdf', '1810.04805.pdf'];
-
- for (const pdfFile of pdfFiles) {
- console.log(`Ingesting ${pdfFile}...`);
- const pdfPath = path.join(__dirname, pdfFile);
- const dataBuffer = fs.readFileSync(pdfPath);
-
- const pages = await pdfToPages(dataBuffer);
-
- for (let i = 0; i < pages.length; i++) {
- const text = pages[i].text.trim();
- if (text) {
- await collection.add({
- ids: [`${pdfFile}-${i}`],
- documents: [text]
- });
- }
- }
- }
-
- console.log('\nIngestion complete!');
- const count = await collection.count();
- console.log(`Total documents in collection: ${count}`);
-}
-
-main().catch(console.error);
-```
-
-
-
-
-
-```python title="Python"
-import os
-import pymongo
-import pypdf
-import requests
-import certifi
-from dotenv import load_dotenv
-
-load_dotenv()
-
-def get_embedding(text, api_key):
- """Get embedding from Hugging Face Inference API"""
- API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5"
- headers = {"Authorization": f"Bearer {api_key}"}
-
- response = requests.post(API_URL, headers=headers, json={"inputs": [text], "options": {"wait_for_model": True}})
-
- if response.status_code == 200:
- return response.json()[0]
- else:
- raise Exception(f"HF API error: {response.status_code} - {response.text}")
-
-def main():
- hf_api_key = os.getenv("HF_API_KEY")
- mongodb_uri = os.getenv("MONGODB_URI")
- db_name = os.getenv("MONGODB_DB_NAME")
-
- if not all([hf_api_key, mongodb_uri, db_name]):
- print("Error: Ensure HF_API_KEY, MONGODB_URI, and MONGODB_DB_NAME are in .env file")
- return
-
- # Connect to MongoDB Atlas using certifi
- client = pymongo.MongoClient(mongodb_uri, tlsCAFile=certifi.where())
- db = client[db_name]
- collection = db["rag_collection"]
-
- # Ingest PDFs
- pdf_files = ["1706.03762.pdf", "1810.04805.pdf"]
- for pdf_file in pdf_files:
- print(f"Ingesting {pdf_file}...")
- reader = pypdf.PdfReader(pdf_file)
- for i, page in enumerate(reader.pages):
- text = page.extract_text()
- if not text: # Skip empty pages
- continue
-
- # Generate embedding using Hugging Face
- print(f" Processing page {i+1}...")
- try:
- embedding = get_embedding(text, hf_api_key)
- collection.insert_one({
- "_id": f"{pdf_file}-{i}",
- "text": text,
- "embedding": embedding,
- "source": pdf_file,
- "page": i
- })
- except Exception as e:
- print(f" Could not process page {i+1}: {e}")
-
-
- print("\nIngestion complete!")
- print(f"Total documents in collection: {collection.count_documents({})}")
-
- # Create vector search index
- print("\nNext: Go to your MongoDB Atlas dashboard and create a search index named 'vector_index'")
- print('''{
- "fields": [
- {
- "type": "vector",
- "path": "embedding",
- "numDimensions": 384,
- "similarity": "cosine"
- }
- ]
-}''')
-
-if __name__ == "__main__":
- main()
-```
-
-```typescript title="TypeScript"
-import { MongoClient } from 'mongodb';
-import * as dotenv from 'dotenv';
-import { pdfToPages } from 'pdf-ts';
-import * as fs from 'fs';
-import fetch from 'node-fetch';
-
-dotenv.config();
-
-async function getEmbedding(text: string, apiKey: string): Promise {
- const API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5";
- const headers = {
- "Authorization": `Bearer ${apiKey}`,
- "Content-Type": "application/json"
- };
-
- const response = await fetch(API_URL, {
- method: 'POST',
- headers: headers,
- body: JSON.stringify({
- inputs: [text],
- options: { wait_for_model: true }
- })
- });
-
- if (response.ok) {
- const result: any = await response.json();
- return result[0];
- } else {
- const errorText = await response.text();
- throw new Error(`HF API error: ${response.status} - ${errorText}`);
- }
-}
-
-async function main() {
- const hfApiKey = process.env.HF_API_KEY || '';
- const mongoUri = process.env.MONGODB_URI || '';
- const dbName = process.env.MONGODB_DB_NAME || '';
-
- if (!hfApiKey || !mongoUri || !dbName) {
- console.error('Error: Ensure HF_API_KEY, MONGODB_URI, and MONGODB_DB_NAME are in .env file');
- return;
- }
-
- // Connect to MongoDB Atlas
- const client = new MongoClient(mongoUri);
-
- try {
- await client.connect();
- console.log('Connected to MongoDB Atlas');
-
- const db = client.db(dbName);
- const collection = db.collection('rag_collection');
-
- // Ingest PDFs
- const pdfFiles = ['1706.03762.pdf', '1810.04805.pdf'];
-
- for (const pdfFile of pdfFiles) {
- console.log(`Ingesting ${pdfFile}...`);
-
- const dataBuffer = fs.readFileSync(pdfFile);
- const pages = await pdfToPages(dataBuffer);
-
- for (let i = 0; i < pages.length; i++) {
- const text = pages[i].text;
-
- if (!text || text.trim().length === 0) {
- continue; // Skip empty pages
- }
-
- // Generate embedding using Hugging Face
- console.log(` Processing page ${i + 1}...`);
- try {
- const embedding = await getEmbedding(text, hfApiKey);
-
- await collection.insertOne({
- _id: `${pdfFile}-${i}`,
- text: text,
- embedding: embedding,
- source: pdfFile,
- page: i
- });
- } catch (error) {
- console.log(` Could not process page ${i + 1}: ${error}`);
- }
- }
- }
-
- const docCount = await collection.countDocuments({});
- console.log('\nIngestion complete!');
- console.log(`Total documents in collection: ${docCount}`);
-
- console.log('\nNext: Go to your MongoDB Atlas dashboard and create a search index named "vector_index"');
- console.log(JSON.stringify({
- "fields": [
- {
- "type": "vector",
- "path": "embedding",
- "numDimensions": 384,
- "similarity": "cosine"
- }
- ]
- }, null, 2));
-
- } catch (error) {
- console.error('Error:', error);
- } finally {
- await client.close();
- }
-}
-
-main();
-```
-
-
-
-
-
-```python title="Python"
-import os
-import pypdf
-import requests
-from dotenv import load_dotenv
-from qdrant_client import QdrantClient
-from qdrant_client.models import Distance, VectorParams, PointStruct
-
-load_dotenv()
-
-def get_embedding(text, api_key):
- """Get embedding from Hugging Face Inference API"""
- API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5"
- headers = {"Authorization": f"Bearer {api_key}"}
-
- response = requests.post(API_URL, headers=headers, json={"inputs": text, "options": {"wait_for_model": True}})
-
- if response.status_code == 200:
- return response.json()
- else:
- raise Exception(f"HF API error: {response.status_code} - {response.text}")
-
-def main():
- hf_api_key = os.getenv("HF_API_KEY")
-
- if not hf_api_key:
- print("Error: HF_API_KEY not found in .env file")
- return
-
- # Connect to Qdrant Cloud
- client = QdrantClient(
- url=os.getenv("QDRANT_URL"),
- api_key=os.getenv("QDRANT_API_KEY")
- )
-
- # Create collection
- collection_name = "rag_collection"
-
- # Check if collection exists, if not create it
- collections = client.get_collections().collections
- if collection_name not in [c.name for c in collections]:
- client.create_collection(
- collection_name=collection_name,
- vectors_config=VectorParams(size=384, distance=Distance.COSINE)
- )
-
- # Ingest PDFs
- pdf_files = ["1706.03762.pdf", "1810.04805.pdf"]
- point_id = 0
-
- for pdf_file in pdf_files:
- print(f"Ingesting {pdf_file}...")
- reader = pypdf.PdfReader(pdf_file)
- for i, page in enumerate(reader.pages):
- text = page.extract_text()
-
- # Generate embedding using Hugging Face
- print(f" Processing page {i+1}...")
- embedding = get_embedding(text, hf_api_key)
-
- client.upsert(
- collection_name=collection_name,
- points=[
- PointStruct(
- id=point_id,
- vector=embedding,
- payload={"text": text, "source": pdf_file, "page": i}
- )
- ]
- )
- point_id += 1
-
- print("\nIngestion complete!")
- collection_info = client.get_collection(collection_name)
- print(f"Total documents in collection: {collection_info.points_count}")
-
-if __name__ == "__main__":
- main()
-```
-
-```typescript title="TypeScript"
-import { QdrantClient } from '@qdrant/js-client-rest';
-import { pdfToPages } from 'pdf-ts';
-import dotenv from 'dotenv';
-import fetch from 'node-fetch';
-import * as fs from 'fs';
-
-dotenv.config();
-
-async function getEmbedding(text: string, apiKey: string): Promise {
- const API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5";
-
- const response = await fetch(API_URL, {
- method: 'POST',
- headers: {
- "Authorization": `Bearer ${apiKey}`,
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- inputs: [text],
- options: { wait_for_model: true }
- })
- });
-
- if (response.ok) {
- const result: any = await response.json();
- return result[0];
- } else {
- const error = await response.text();
- throw new Error(`HuggingFace API error: ${response.status} - ${error}`);
- }
-}
-
-async function main() {
- const hfApiKey = process.env.HF_API_KEY || '';
-
- if (!hfApiKey) {
- console.error('Error: HF_API_KEY not found in .env file');
- return;
- }
-
- // Connect to Qdrant Cloud
- const client = new QdrantClient({
- url: process.env.QDRANT_URL || '',
- apiKey: process.env.QDRANT_API_KEY || ''
- });
-
- const collectionName = 'rag_collection';
-
- // Check if collection exists, if not create it
- const collections = await client.getCollections();
- const collectionExists = collections.collections.some(c => c.name === collectionName);
-
- if (!collectionExists) {
- console.log('Creating collection...');
- await client.createCollection(collectionName, {
- vectors: {
- size: 384,
- distance: 'Cosine'
- }
- });
- }
-
- // Ingest PDFs
- const pdfFiles = ['1706.03762.pdf', '1810.04805.pdf'];
- let pointId = 0;
-
- for (const pdfFile of pdfFiles) {
- console.log(`\nIngesting ${pdfFile}...`);
- const dataBuffer = fs.readFileSync(pdfFile);
- const pages = await pdfToPages(dataBuffer);
-
- for (let i = 0; i < pages.length; i++) {
- const text = pages[i].text;
-
- console.log(` Processing page ${i + 1}...`);
- const embedding = await getEmbedding(text, hfApiKey);
-
- await client.upsert(collectionName, {
- wait: true,
- points: [
- {
- id: pointId,
- vector: embedding,
- payload: {
- text: text,
- source: pdfFile,
- page: i
- }
- }
- ]
- });
- pointId++;
- }
- }
-
- console.log('\nIngestion complete!');
- const collectionInfo = await client.getCollection(collectionName);
- console.log(`Total documents in collection: ${collectionInfo.points_count}`);
-}
-
-main().catch(console.error);
-```
-
-
-
-
-Run the script from your terminal:
-
-
-
-```bash
-python setup.py
-```
-
-
-```bash
-npx tsx setup.ts
-```
-
-
-
-If you are using MongoDB Atlas, you must manually create a vector search index by following the steps below.
-
-
-
-**MongoDB Atlas users:** The setup script ingests your data, but MongoDB Atlas requires you to manually create a vector search index before queries will work. Follow these steps carefully.
-
-
-
-
- Log in to your [MongoDB Atlas dashboard](https://cloud.mongodb.com/), and click on **"Search & Vector Search"** in the sidebar.
-
-
- Click **"Create Search Index"**, choose Vector Search.
-
-
- - Database: Select **`rag_demo`** (or whatever you set as `MONGODB_DB_NAME`)
- - Collection: Select **`rag_collection`**
-
-
- - Index Name: Enter **`vector_index`** (this exact name is required by the code)
- - Choose **"JSON Editor"** (not "Visual Editor"). Click **Next**
- - Paste this JSON definition:
- ```json
- {
- "fields": [
- {
- "type": "vector",
- "path": "embedding",
- "numDimensions": 384,
- "similarity": "cosine"
- }
- ]
- }
- ```
- **Note:** 384 dimensions is for Hugging Face's `BAAI/bge-small-en-v1.5` model.
-
-
- Click **Next**, then click **"Create Search Index"**. The index will take a few minutes to build. Wait until the status shows as **"Active"** before proceeding.
-
-
-
-
-Your vector database is now populated with research paper content and ready to query.
-
-## Step 2: Create a Simple Letta Agent
-
-For the Simple RAG approach, the Letta agent doesn't need any special tools or complex instructions. Its only job is to answer a question based on the context we provide. We can create this agent programmatically using the Letta SDK.
-
-Create a file named `create_agent.py` or `create_agent.ts`:
-
-
-```python
-import os
-from letta_client import Letta
-from dotenv import load_dotenv
-
-load_dotenv()
-
-# Initialize the Letta client
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# Create the agent
-agent = client.agents.create(
- name="Simple RAG Agent",
- description="This agent answers questions based on provided context. It has no tools or special memory.",
- memory_blocks=[
- {
- "label": "persona",
- "value": "You are a helpful research assistant. Answer the user's question based *only* on the context provided."
- }
- ]
-)
-
-print(f"Agent '{agent.name}' created with ID: {agent.id}")
-```
-
-
-```typescript
-import { LettaClient } from '@letta-ai/letta-client';
-import * as dotenv from 'dotenv';
-
-dotenv.config();
-
-async function main() {
- // Initialize the Letta client
- const client = new LettaClient({
- token: process.env.LETTA_API_KEY || ''
- });
-
- // Create the agent
- const agent = await client.agents.create({
- name: 'Simple RAG Agent',
- description: 'This agent answers questions based on provided context. It has no tools or special memory.',
- memoryBlocks: [
- {
- label: 'persona',
- value: 'You are a helpful research assistant. Answer the user\'s question based *only* on the context provided.'
- }
- ]
- });
-
- console.log(`Agent '${agent.name}' created with ID: ${agent.id}`);
-}
-
-main().catch(console.error);
-```
-
-
-
-Run this script once to create the agent in your Letta project.
-
-
-```bash title="Python"
-python create_agent.py```
-
-```bash title="TypeScript"
-npx tsx create_agent.ts
-```
-
-
-
-
-## Step 3: Query, Format, and Ask
-
-Now we'll write the main script, `simple_rag.py` or `simple_rag.ts`, that ties everything together. This script will:
-
-1. Take a user's question.
-2. Query your vector database to find the most relevant document chunks.
-3. Construct a detailed prompt that includes both the user's question and the retrieved context.
-4. Send this combined prompt to our Simple Letta agent and print the response.
-
-
-
-
-```python title="Python"
-import os
-import chromadb
-from letta_client import Letta
-from dotenv import load_dotenv
-
-load_dotenv()
-
-# Initialize clients
-letta_client = Letta(token=os.getenv("LETTA_API_KEY"))
-chroma_client = chromadb.CloudClient(
- tenant=os.getenv("CHROMA_TENANT"),
- database=os.getenv("CHROMA_DATABASE"),
- api_key=os.getenv("CHROMA_API_KEY")
-)
-
-AGENT_ID = "your-agent-id" # Replace with your agent ID
-
-def main():
- while True:
- question = input("\nAsk a question about the research papers: ")
- if question.lower() in ['exit', 'quit']:
- break
-
- # 1. Query ChromaDB
- collection = chroma_client.get_collection("rag_collection")
- results = collection.query(query_texts=[question], n_results=3)
- context = "\n".join(results["documents"][0])
-
- # 2. Construct the prompt
- prompt = f'''Context from research paper:
-{context}
-
-Question: {question}
-
-Answer:'''
-
- # 3. Send to Letta Agent
- response = letta_client.agents.messages.create(
- agent_id=AGENT_ID,
- messages=[{"role": "user", "content": prompt}]
- )
-
- for message in response.messages:
- if message.message_type == 'assistant_message':
- print(f"\nAgent: {message.content}")
-
-if __name__ == "__main__":
- main()
-```
-
-```typescript title="TypeScript"
-import { LettaClient } from '@letta-ai/letta-client';
-import { CloudClient } from 'chromadb';
-import { DefaultEmbeddingFunction } from '@chroma-core/default-embed';
-import * as dotenv from 'dotenv';
-import * as readline from 'readline';
-
-dotenv.config();
-
-const AGENT_ID = 'your-agent-id'; // Replace with your agent ID
-
-// Initialize clients
-const lettaClient = new LettaClient({
- token: process.env.LETTA_API_KEY || ''
-});
-
-const chromaClient = new CloudClient({
- apiKey: process.env.CHROMA_API_KEY || '',
- tenant: process.env.CHROMA_TENANT || '',
- database: process.env.CHROMA_DATABASE || ''
-});
-
-async function main() {
- const embedder = new DefaultEmbeddingFunction();
- const collection = await chromaClient.getCollection({
- name: 'rag_collection',
- embeddingFunction: embedder
- });
-
- const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout
- });
-
- const askQuestion = () => {
- rl.question('\nAsk a question about the research papers (or type "exit" to quit): ', async (question) => {
- if (question.toLowerCase() === 'exit' || question.toLowerCase() === 'quit') {
- rl.close();
- return;
- }
-
- // 1. Query ChromaDB
- const results = await collection.query({
- queryTexts: [question],
- nResults: 3
- });
-
- const context = results.documents[0].join('\n');
-
- // 2. Construct the prompt
- const prompt = `Context from research paper:
-${context}
-
-Question: ${question}
-
-Answer:`;
-
- // 3. Send to Letta Agent
- const response = await lettaClient.agents.messages.create(AGENT_ID, {
- messages: [{ role: 'user', content: prompt }]
- });
-
- for (const message of response.messages) {
- if (message.messageType === 'assistant_message') {
- console.log(`\nAgent: ${(message as any).content}`);
- }
- }
-
- askQuestion();
- });
- };
-
- askQuestion();
-}
-
-main().catch(console.error);
-```
-
-
-
-
-
-```python title="Python"
-import os
-import pymongo
-import requests
-import certifi
-from letta_client import Letta
-from dotenv import load_dotenv
-
-load_dotenv()
-
-def get_embedding(text, api_key):
- """Get embedding from Hugging Face Inference API"""
- API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5"
- headers = {"Authorization": f"Bearer {api_key}"}
- response = requests.post(API_URL, headers=headers, json={"inputs": [text], "options": {"wait_for_model": True}})
-
- if response.status_code == 200:
- return response.json()[0]
- else:
- raise Exception(f"HuggingFace API error: {response.status_code} - {response.text}")
-
-# Initialize clients
-letta_client = Letta(token=os.getenv("LETTA_API_KEY"))
-mongo_client = pymongo.MongoClient(os.getenv("MONGODB_URI"), tlsCAFile=certifi.where())
-db = mongo_client[os.getenv("MONGODB_DB_NAME")]
-collection = db["rag_collection"]
-hf_api_key = os.getenv("HF_API_KEY")
-
-AGENT_ID = "your-agent-id" # Replace with your agent ID
-
-def main():
- while True:
- question = input("\nAsk a question about the research papers: ")
- if question.lower() in ['exit', 'quit']:
- break
-
- # 1. Query MongoDB Atlas Vector Search
- query_embedding = get_embedding(question, hf_api_key)
-
- results = collection.aggregate([
- {
- "$vectorSearch": {
- "index": "vector_index",
- "path": "embedding",
- "queryVector": query_embedding,
- "numCandidates": 100,
- "limit": 3
- }
- },
- {
- "$project": {
- "text": 1,
- "source": 1,
- "page": 1,
- "score": {"$meta": "vectorSearchScore"}
- }
- }
- ])
-
- contexts = [doc.get("text", "") for doc in results]
- context = "\n\n".join(contexts)
-
- # 2. Construct the prompt
- prompt = f'''Context from research paper:
-{context}
-
-Question: {question}
-
-Answer:'''
-
- # 3. Send to Letta Agent
- response = letta_client.agents.messages.create(
- agent_id=AGENT_ID,
- messages=[{"role": "user", "content": prompt}]
- )
-
- for message in response.messages:
- if message.message_type == 'assistant_message':
- print(f"\nAgent: {message.content}")
-
-if __name__ == "__main__":
- main()
-```
-
-```typescript title="TypeScript"
-import { LettaClient } from '@letta-ai/letta-client';
-import { MongoClient } from 'mongodb';
-import * as dotenv from 'dotenv';
-import * as readline from 'readline';
-import fetch from 'node-fetch';
-
-dotenv.config();
-
-const AGENT_ID = 'your-agent-id'; // Replace with your agent ID
-
-async function getEmbedding(text: string, apiKey: string): Promise {
- const API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5";
- const headers = {
- "Authorization": `Bearer ${apiKey}`,
- "Content-Type": "application/json"
- };
-
- const response = await fetch(API_URL, {
- method: 'POST',
- headers: headers,
- body: JSON.stringify({
- inputs: [text],
- options: { wait_for_model: true }
- })
- });
-
- if (response.ok) {
- const result: any = await response.json();
- return result[0];
- } else {
- const errorText = await response.text();
- throw new Error(`HuggingFace API error: ${response.status} - ${errorText}`);
- }
-}
-
-async function main() {
- const lettaApiKey = process.env.LETTA_API_KEY || '';
- const mongoUri = process.env.MONGODB_URI || '';
- const dbName = process.env.MONGODB_DB_NAME || '';
- const hfApiKey = process.env.HF_API_KEY || '';
-
- if (!lettaApiKey || !mongoUri || !dbName || !hfApiKey) {
- console.error('Error: Ensure LETTA_API_KEY, MONGODB_URI, MONGODB_DB_NAME, and HF_API_KEY are in .env file');
- return;
- }
-
- // Initialize clients
- const lettaClient = new LettaClient({
- token: lettaApiKey
- });
-
- const mongoClient = new MongoClient(mongoUri);
- await mongoClient.connect();
- console.log('Connected to MongoDB Atlas\n');
-
- const db = mongoClient.db(dbName);
- const collection = db.collection('rag_collection');
-
- const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout
- });
-
- const askQuestion = () => {
- rl.question('\nAsk a question about the research papers (or type "exit" to quit): ', async (question) => {
- if (question.toLowerCase() === 'exit' || question.toLowerCase() === 'quit') {
- await mongoClient.close();
- rl.close();
- return;
- }
-
- try {
- // 1. Query MongoDB Atlas Vector Search
- const queryEmbedding = await getEmbedding(question, hfApiKey);
-
- const results = collection.aggregate([
- {
- $vectorSearch: {
- index: 'vector_index',
- path: 'embedding',
- queryVector: queryEmbedding,
- numCandidates: 100,
- limit: 3
- }
- },
- {
- $project: {
- text: 1,
- source: 1,
- page: 1,
- score: { $meta: 'vectorSearchScore' }
- }
- }
- ]);
-
- const docs = await results.toArray();
- const contexts = docs.map(doc => doc.text || '');
- const context = contexts.join('\n\n');
-
- // 2. Construct the prompt
- const prompt = `Context from research paper:
-${context}
-
-Question: ${question}
-
-Answer:`;
-
- // 3. Send to Letta Agent
- const response = await lettaClient.agents.messages.create(AGENT_ID, {
- messages: [{ role: 'user', content: prompt }]
- });
-
- for (const message of response.messages) {
- if (message.messageType === 'assistant_message') {
- console.log(`\nAgent: ${(message as any).content}`);
- }
- }
-
- } catch (error) {
- console.error('Error:', error);
- }
-
- askQuestion();
- });
- };
-
- askQuestion();
-}
-
-main().catch(console.error);
-```
-
-
-
-
-
-```python title="Python"
-import os
-import requests
-from letta_client import Letta
-from dotenv import load_dotenv
-from qdrant_client import QdrantClient
-
-load_dotenv()
-
-def get_embedding(text, api_key):
- """Get embedding from Hugging Face Inference API"""
- API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5"
- headers = {"Authorization": f"Bearer {api_key}"}
- response = requests.post(API_URL, headers=headers, json={"inputs": text, "options": {"wait_for_model": True}})
-
- if response.status_code == 200:
- return response.json()
- else:
- raise Exception(f"HuggingFace API error: {response.status_code} - {response.text}")
-
-# Initialize clients
-letta_client = Letta(token=os.getenv("LETTA_API_KEY"))
-qdrant_client = QdrantClient(
- url=os.getenv("QDRANT_URL"),
- api_key=os.getenv("QDRANT_API_KEY")
-)
-hf_api_key = os.getenv("HF_API_KEY")
-
-AGENT_ID = "your-agent-id" # Replace with your agent ID
-
-def main():
- while True:
- question = input("\nAsk a question about the research papers: ")
- if question.lower() in ['exit', 'quit']:
- break
-
- # 1. Query Qdrant
- query_embedding = get_embedding(question, hf_api_key)
-
- results = qdrant_client.query_points(
- collection_name="rag_collection",
- query=query_embedding,
- limit=3
- )
-
- contexts = [hit.payload["text"] for hit in results.points]
- context = "\n".join(contexts)
-
- # 2. Construct the prompt
- prompt = f'''Context from research paper:
-{context}
-
-Question: {question}
-
-Answer:'''
-
- # 3. Send to Letta Agent
- response = letta_client.agents.messages.create(
- agent_id=AGENT_ID,
- messages=[{"role": "user", "content": prompt}]
- )
-
- for message in response.messages:
- if message.message_type == 'assistant_message':
- print(f"\nAgent: {message.content}")
-
-if __name__ == "__main__":
- main()
-```
-
-```typescript title="TypeScript"
-import { QdrantClient } from '@qdrant/js-client-rest';
-import { LettaClient } from '@letta-ai/letta-client';
-import dotenv from 'dotenv';
-import fetch from 'node-fetch';
-import * as readline from 'readline';
-
-dotenv.config();
-
-async function getEmbedding(text: string, apiKey: string): Promise {
- const API_URL = "https://api-inference.huggingface.co/models/BAAI/bge-small-en-v1.5";
-
- const response = await fetch(API_URL, {
- method: 'POST',
- headers: {
- "Authorization": `Bearer ${apiKey}`,
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- inputs: [text],
- options: { wait_for_model: true }
- })
- });
-
- if (response.ok) {
- const result: any = await response.json();
- return result[0];
- } else {
- const error = await response.text();
- throw new Error(`HuggingFace API error: ${response.status} - ${error}`);
- }
-}
-
-async function main() {
- // Initialize clients
- const lettaClient = new LettaClient({
- token: process.env.LETTA_API_KEY || ''
- });
-
- const qdrantClient = new QdrantClient({
- url: process.env.QDRANT_URL || '',
- apiKey: process.env.QDRANT_API_KEY || ''
- });
-
- const hfApiKey = process.env.HF_API_KEY || '';
- const AGENT_ID = 'your-agent-id'; // Replace with your agent ID
-
- const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout
- });
-
- const askQuestion = (query: string): Promise => {
- return new Promise((resolve) => {
- rl.question(query, resolve);
- });
- };
-
- while (true) {
- const question = await askQuestion('\nAsk a question about the research papers (or type "exit" to quit): ');
-
- if (question.toLowerCase() === 'exit' || question.toLowerCase() === 'quit') {
- rl.close();
- break;
- }
-
- // 1. Query Qdrant
- const queryEmbedding = await getEmbedding(question, hfApiKey);
-
- const results = await qdrantClient.query(
- 'rag_collection',
- {
- query: queryEmbedding,
- limit: 3,
- with_payload: true
- }
- );
-
- const contexts = results.points.map((hit: any) => hit.payload.text);
- const context = contexts.join('\n');
-
- // 2. Construct the prompt
- const prompt = `Context from research paper:
-${context}
-
-Question: ${question}
-
-Answer:`;
-
- // 3. Send to Letta Agent
- const response = await lettaClient.agents.messages.create(AGENT_ID, {
- messages: [{ role: 'user', content: prompt }]
- });
-
- for (const message of response.messages) {
- if (message.messageType === 'assistant_message') {
- console.log(`\nAgent: ${(message as any).content}`);
- }
- }
- }
-}
-
-main().catch(console.error);
-```
-
-
-
-
-
-Replace `your-agent-id` with the actual ID of the agent you created in the previous step.
-
-
-When you run this script, your application performs the retrieval, and the Letta agent provides the answer based on the context it receives. This gives you full control over the data pipeline.
-
-## Next Steps
-
-Now that you've integrated Simple RAG with Letta, you can explore more advanced integration patterns:
-
-
-
- Learn how to empower your agent with custom search tools for autonomous retrieval.
-
-
- Explore creating more advanced custom tools for your agents.
-
-
diff --git a/fern/pages/cookbooks_simple.mdx b/fern/pages/cookbooks_simple.mdx
deleted file mode 100644
index 0cf3e5af..00000000
--- a/fern/pages/cookbooks_simple.mdx
+++ /dev/null
@@ -1,274 +0,0 @@
----
-title: Examples & Tutorials
-slug: cookbooks
----
-
-Build powerful AI agents with persistent memory. Explore tutorials, ready-to-use templates, and community projects to get started.
-
-
-**New to Letta?**
-
-- Start with our [Quickstart Guide](/quickstart)
-- Take the free [DeepLearning.AI Course](https://www.deeplearning.ai/short-courses/llms-as-operating-systems-agent-memory/)
-- Explore [Awesome Letta](https://github.com/letta-ai/awesome-letta) for more resources
-
-
-## Getting Started Tutorials
-
-Step-by-step guides to learn Letta fundamentals.
-
-
-
-Build your first Letta agent in minutes
-
-
-Create an agent that can answer questions about PDF documents
-
-
-Learn how to dynamically manage agent memory
-
-
-Share memory between multiple agents for coordination
-
-
-
-## Ready-to-Deploy Applications
-
-Production-ready templates you can clone and customize.
-
-
-
-Full-stack chatbot with per-user agent memory (Next.js + TypeScript)
-
-
-Discord bot with persistent memory for each server and user
-
-
-Create AI characters with memory that persists across conversations
-
-
-Research agent that gathers and synthesizes information over time
-
-
-
-## Multi-Agent Systems
-
-Build coordinated teams of specialized agents.
-
-
-
-Connect agents to chat with each other and users simultaneously
-
-
-Template for building relationship-aware agents for each customer
-
-
-
-## Tools & Integrations
-
-Connect Letta to your favorite platforms and tools.
-
-
-
-Use Letta with Vercel AI SDK v5
-
-
-Connect agents to 7,000+ apps
-
-
-Integrate with n8n automation workflows
-
-
-Deploy agents on Telegram
-
-
-Add Letta agents to your knowledge base
-
-
-SQL-powered data analysis agent
-
-
-
-## SDK Examples
-
-Learn the basics with minimal code examples.
-
-
-
-Basic TypeScript/Node.js SDK example
-
-
-Basic Python SDK example
-
-
-
-## Community Projects
-
-Amazing projects built by the Letta community.
-
-
-
-Deploy Letta agents to an ATProto-powered multi-agent chatroom
-
-
-IRC-style CLI for the Thought Stream
-
-
-
-## Learning Resources
-
-
-
-Free course: LLMs as Operating Systems - Building Agents with Memory
-
-
-Understand how Letta agents work
-
-
-Complete API documentation
-
-
-Read about the research behind Letta
-
-
-
-## More Resources
-
-
-
-Comprehensive curated list of Letta resources, tools, and community projects
-
-
-Get help and share your projects with the community
-
-
diff --git a/fern/pages/evals/README.md b/fern/pages/evals/README.md
deleted file mode 100644
index 74a6c2b5..00000000
--- a/fern/pages/evals/README.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# Letta Evals Documentation
-
-Welcome to the comprehensive documentation for Letta Evals Kit - a framework for evaluating Letta AI agents.
-
-## Table of Contents
-
-### Getting Started
-- [Getting Started](./getting-started.md) - Installation, first evaluation, and core concepts
-
-### Core Concepts
-- [Overview](./concepts/overview.md) - Understanding the evaluation framework
-- [Suites](./concepts/suites.md) - Evaluation suite configuration
-- [Datasets](./concepts/datasets.md) - Creating and managing test datasets
-- [Targets](./concepts/targets.md) - What you're evaluating
-- [Graders](./concepts/graders.md) - How responses are scored
-- [Extractors](./concepts/extractors.md) - Extracting submissions from agent output
-- [Gates](./concepts/gates.md) - Pass/fail criteria
-
-### Graders
-- [Grader Overview](./graders/overview.md) - Understanding grader types
-- [Tool Graders](./graders/tool-graders.md) - Built-in and custom function graders
-- [Rubric Graders](./graders/rubric-graders.md) - LLM-as-judge evaluation
-- [Multi-Metric Grading](./graders/multi-metric.md) - Evaluating with multiple metrics
-
-### Extractors
-- [Extractor Overview](./extractors/overview.md) - Understanding extractors
-- [Built-in Extractors](./extractors/builtin.md) - All available extractors
-- [Custom Extractors](./extractors/custom.md) - Writing your own extractors
-
-### Configuration
-- [Suite YAML Reference](./configuration/suite-yaml.md) - Complete YAML schema
-- [Target Configuration](./configuration/targets.md) - Target setup options
-- [Grader Configuration](./configuration/graders.md) - Grader parameters
-- [Environment Variables](./configuration/environment.md) - Environment setup
-
-### Advanced Usage
-- [Custom Graders](./advanced/custom-graders.md) - Writing custom grading functions
-- [Multi-Turn Conversations](./advanced/multi-turn-conversations.md) - Testing conversational memory and state
-- [Agent Factories](./advanced/agent-factories.md) - Programmatic agent creation
-- [Multi-Model Evaluation](./advanced/multi-model.md) - Testing across models
-- [Setup Scripts](./advanced/setup-scripts.md) - Pre-evaluation setup
-- [Memory Block Testing](./advanced/memory-blocks.md) - Testing agent memory
-- [Result Streaming](./advanced/streaming.md) - Real-time results and caching
-
-### Results & Metrics
-- [Understanding Results](./results/overview.md) - Result structure and interpretation
-- [Metrics](./results/metrics.md) - Aggregate statistics
-- [Output Formats](./results/output-formats.md) - JSON, JSONL, and console output
-
-### CLI Reference
-- [Commands](./cli/commands.md) - All CLI commands
-- [Options](./cli/options.md) - Command-line options
-
-### Examples
-- [Example Walkthroughs](./examples/README.md) - Detailed example explanations
-
-### API Reference
-- [Data Models](./api/models.md) - Pydantic models reference
-- [Decorators](./api/decorators.md) - @grader and @extractor decorators
-
-### Best Practices
-- [Writing Effective Tests](./best-practices/writing-tests.md)
-- [Designing Rubrics](./best-practices/rubrics.md)
-- [Performance Optimization](./best-practices/performance.md)
-
-### Troubleshooting
-- [Common Issues](./troubleshooting.md)
-- [FAQ](./faq.md)
diff --git a/fern/pages/evals/advanced/custom-graders.md b/fern/pages/evals/advanced/custom-graders.md
deleted file mode 100644
index 12d1626b..00000000
--- a/fern/pages/evals/advanced/custom-graders.md
+++ /dev/null
@@ -1,425 +0,0 @@
-# Custom Graders
-
-Write your own grading functions to implement custom evaluation logic.
-
-## Overview
-
-Custom graders let you:
-- Implement domain-specific evaluation
-- Parse and validate complex formats
-- Apply custom scoring algorithms
-- Combine multiple checks in one grader
-
-## Basic Structure
-
-```python
-from letta_evals.decorators import grader
-from letta_evals.models import GradeResult, Sample
-
-@grader
-def my_custom_grader(sample: Sample, submission: str) -> GradeResult:
- """Your custom grading logic."""
- # Evaluate the submission
- score = calculate_score(submission, sample)
-
- return GradeResult(
- score=score, # Must be 0.0 to 1.0
- rationale="Explanation of the score",
- metadata={"extra": "information"}
- )
-```
-
-## The @grader Decorator
-
-The `@grader` decorator registers your function so it can be used in suite YAML:
-
-```python
-from letta_evals.decorators import grader
-
-@grader # Makes this function available as "my_function"
-def my_function(sample: Sample, submission: str) -> GradeResult:
- ...
-```
-
-Without the decorator, your function won't be discovered.
-
-## Function Signature
-
-Your grader must have this signature:
-
-```python
-def grader_name(sample: Sample, submission: str) -> GradeResult:
- ...
-```
-
-### Parameters
-
-- `sample`: The dataset sample being evaluated (includes `input`, `ground_truth`, `metadata`, etc.)
-- `submission`: The extracted text from the agent's response
-
-### Return Value
-
-Must return a `GradeResult`:
-
-```python
-from letta_evals.models import GradeResult
-
-return GradeResult(
- score=0.85, # Required: 0.0 to 1.0
- rationale="Explanation", # Optional but recommended
- metadata={"key": "value"} # Optional: any extra data
-)
-```
-
-## Complete Example
-
-```python
-# custom_graders.py
-import json
-from letta_evals.decorators import grader
-from letta_evals.models import GradeResult, Sample
-
-@grader
-def json_field_validator(sample: Sample, submission: str) -> GradeResult:
- """Validates JSON and checks for required fields."""
- required_fields = sample.ground_truth.split(",") # e.g., "name,age,email"
-
- try:
- data = json.loads(submission)
- except json.JSONDecodeError as e:
- return GradeResult(
- score=0.0,
- rationale=f"Invalid JSON: {e}",
- metadata={"error": "json_decode"}
- )
-
- missing = [f for f in required_fields if f not in data]
-
- if missing:
- score = 1.0 - (len(missing) / len(required_fields))
- return GradeResult(
- score=score,
- rationale=f"Missing fields: {', '.join(missing)}",
- metadata={"missing_fields": missing}
- )
-
- return GradeResult(
- score=1.0,
- rationale="All required fields present",
- metadata={"fields_found": required_fields}
- )
-```
-
-Dataset:
-```jsonl
-{"input": "Return user info as JSON", "ground_truth": "name,age,email"}
-```
-
-Suite:
-```yaml
-graders:
- json_check:
- kind: tool
- function: json_field_validator
- extractor: last_assistant
-```
-
-## Using Custom Graders
-
-### Method 1: Custom Evaluators File
-
-Create a file with your graders (e.g., `custom_evaluators.py`) in your project:
-
-```python
-from letta_evals.decorators import grader
-from letta_evals.models import GradeResult, Sample
-
-@grader
-def my_grader(sample: Sample, submission: str) -> GradeResult:
- ...
-```
-
-Reference it in your suite:
-
-```yaml
-# The file will be automatically discovered if it's in the same directory
-# or use Python path imports
-graders:
- my_metric:
- kind: tool
- function: my_grader
- extractor: last_assistant
-```
-
-### Method 2: Setup Script
-
-Import your graders in a setup script:
-
-```python
-# setup.py
-from letta_evals.models import SuiteSpec
-import custom_evaluators # This imports and registers graders
-
-def prepare_environment(suite: SuiteSpec) -> None:
- pass # Graders are registered via import
-```
-
-```yaml
-setup_script: setup.py:prepare_environment
-
-graders:
- my_metric:
- kind: tool
- function: my_grader
- extractor: last_assistant
-```
-
-## Real-World Examples
-
-### Length Check
-
-```python
-@grader
-def appropriate_length(sample: Sample, submission: str) -> GradeResult:
- """Check if response length is within expected range."""
- min_len = 50
- max_len = 500
- length = len(submission)
-
- if min_len <= length <= max_len:
- score = 1.0
- rationale = f"Length {length} is appropriate"
- elif length < min_len:
- score = max(0.0, length / min_len)
- rationale = f"Too short: {length} chars (min {min_len})"
- else:
- score = max(0.0, 1.0 - (length - max_len) / max_len)
- rationale = f"Too long: {length} chars (max {max_len})"
-
- return GradeResult(score=score, rationale=rationale)
-```
-
-### Keyword Coverage
-
-```python
-@grader
-def keyword_coverage(sample: Sample, submission: str) -> GradeResult:
- """Check what percentage of required keywords are present."""
- keywords = sample.ground_truth.split(",")
- submission_lower = submission.lower()
-
- found = [kw for kw in keywords if kw.lower() in submission_lower]
- score = len(found) / len(keywords) if keywords else 0.0
-
- return GradeResult(
- score=score,
- rationale=f"Found {len(found)}/{len(keywords)} keywords: {', '.join(found)}",
- metadata={"found": found, "missing": list(set(keywords) - set(found))}
- )
-```
-
-Dataset:
-```jsonl
-{"input": "Explain photosynthesis", "ground_truth": "light,energy,chlorophyll,oxygen,carbon dioxide"}
-```
-
-### Tool Call Validation
-
-```python
-import json
-
-@grader
-def correct_tool_arguments(sample: Sample, submission: str) -> GradeResult:
- """Validate tool was called with correct arguments."""
- try:
- args = json.loads(submission)
- except json.JSONDecodeError:
- return GradeResult(score=0.0, rationale="No valid tool call found")
-
- expected_tool = sample.metadata.get("expected_tool")
- if args.get("tool_name") != expected_tool:
- return GradeResult(
- score=0.0,
- rationale=f"Wrong tool: expected {expected_tool}, got {args.get('tool_name')}"
- )
-
- # Check arguments
- expected_args = json.loads(sample.ground_truth)
- matches = all(args.get(k) == v for k, v in expected_args.items())
-
- if matches:
- return GradeResult(score=1.0, rationale="Tool called with correct arguments")
- else:
- return GradeResult(score=0.5, rationale="Tool correct but arguments differ")
-```
-
-### Numeric Range Check
-
-```python
-@grader
-def numeric_range(sample: Sample, submission: str) -> GradeResult:
- """Check if extracted number is within expected range."""
- try:
- value = float(submission.strip())
- min_val, max_val = map(float, sample.ground_truth.split(","))
-
- if min_val <= value <= max_val:
- return GradeResult(
- score=1.0,
- rationale=f"Value {value} is within range [{min_val}, {max_val}]"
- )
- else:
- # Partial credit based on distance
- if value < min_val:
- distance = min_val - value
- else:
- distance = value - max_val
-
- score = max(0.0, 1.0 - (distance / max_val))
- return GradeResult(
- score=score,
- rationale=f"Value {value} outside range [{min_val}, {max_val}]"
- )
-
- except ValueError as e:
- return GradeResult(score=0.0, rationale=f"Invalid numeric value: {e}")
-```
-
-### Multi-Criteria
-
-```python
-@grader
-def comprehensive_check(sample: Sample, submission: str) -> GradeResult:
- """Multiple checks with weighted scoring."""
- points = 0.0
- issues = []
-
- # Check 1: Contains answer (40%)
- if sample.ground_truth.lower() in submission.lower():
- points += 0.4
- else:
- issues.append("Missing expected answer")
-
- # Check 2: Appropriate length (20%)
- if 100 <= len(submission) <= 500:
- points += 0.2
- else:
- issues.append(f"Length {len(submission)} not in range [100, 500]")
-
- # Check 3: Starts with capital letter (10%)
- if submission and submission[0].isupper():
- points += 0.1
- else:
- issues.append("Doesn't start with capital letter")
-
- # Check 4: Ends with punctuation (10%)
- if submission and submission[-1] in ".!?":
- points += 0.1
- else:
- issues.append("Doesn't end with punctuation")
-
- # Check 5: No profanity (20%)
- profanity = ["badword1", "badword2"]
- if not any(word in submission.lower() for word in profanity):
- points += 0.2
- else:
- issues.append("Contains inappropriate language")
-
- rationale = f"Score: {points:.2f}. " + (
- "All checks passed!" if not issues else f"Issues: {'; '.join(issues)}"
- )
-
- return GradeResult(
- score=points,
- rationale=rationale,
- metadata={"issues": issues}
- )
-```
-
-## Accessing Sample Data
-
-The `Sample` object provides:
-
-```python
-sample.id # Sample ID
-sample.input # Input (str or List[str])
-sample.ground_truth # Expected answer (optional)
-sample.metadata # Dict with custom data (optional)
-sample.agent_args # Agent creation args (optional)
-```
-
-Use these for flexible grading logic:
-
-```python
-@grader
-def context_aware_grader(sample: Sample, submission: str) -> GradeResult:
- category = sample.metadata.get("category", "general")
-
- if category == "math":
- # Strict for math
- return exact_math_check(sample, submission)
- elif category == "creative":
- # Lenient for creative
- return length_and_relevance_check(sample, submission)
- else:
- return default_check(sample, submission)
-```
-
-## Error Handling
-
-Always handle exceptions:
-
-```python
-@grader
-def safe_grader(sample: Sample, submission: str) -> GradeResult:
- try:
- # Your logic here
- score = complex_calculation(submission)
- return GradeResult(score=score, rationale="Success")
-
- except Exception as e:
- # Return 0.0 with error message
- return GradeResult(
- score=0.0,
- rationale=f"Error during grading: {str(e)}",
- metadata={"error": str(e), "error_type": type(e).__name__}
- )
-```
-
-This ensures evaluation continues even if individual samples fail.
-
-## Testing Your Grader
-
-Test your grader with sample data:
-
-```python
-from letta_evals.models import Sample, GradeResult
-
-# Test case
-sample = Sample(
- id=0,
- input="What is 2+2?",
- ground_truth="4"
-)
-
-submission = "The answer is 4"
-
-result = my_grader(sample, submission)
-print(f"Score: {result.score}, Rationale: {result.rationale}")
-```
-
-## Best Practices
-
-1. **Validate input**: Check for edge cases (empty strings, malformed data)
-2. **Use meaningful rationales**: Explain why a score was given
-3. **Handle errors gracefully**: Return 0.0 with error message rather than crashing
-4. **Keep it fast**: Custom graders run for every sample
-5. **Use metadata**: Store extra information for debugging
-6. **Normalize scores**: Always return 0.0 to 1.0
-7. **Document your grader**: Add docstrings explaining criteria
-
-## Next Steps
-
-- [Custom Extractors](../extractors/custom.md)
-- [Tool Graders](../graders/tool-graders.md)
-- [Examples](../examples/README.md)
diff --git a/fern/pages/evals/advanced/multi-turn-conversations.md b/fern/pages/evals/advanced/multi-turn-conversations.md
deleted file mode 100644
index 3003402a..00000000
--- a/fern/pages/evals/advanced/multi-turn-conversations.md
+++ /dev/null
@@ -1,216 +0,0 @@
-# Multi-Turn Conversations
-
-Multi-turn conversations allow you to test how agents handle context across multiple exchanges - a key capability for stateful agents.
-
-## Why Use Multi-Turn?
-
-Multi-turn conversations enable testing that single-turn prompts cannot:
-
-- **Memory storage**: Verify agents persist information to memory blocks across turns
-- **Tool call sequences**: Test multi-step workflows (e.g., search → analyze → summarize)
-- **Context retention**: Ensure agents remember details from earlier in the conversation
-- **State evolution**: Track how agent state changes across interactions
-- **Conversational coherence**: Test if agents maintain context appropriately
-
-This is essential for stateful agents where behavior depends on conversation history.
-
-## Single vs Multi-Turn Format
-
-### Single-Turn (Default)
-
-Most evaluations use a single prompt:
-
-```jsonl
-{"input": "What is the capital of France?", "ground_truth": "Paris"}
-```
-
-The agent receives one message and responds. Single-turn conversations are useful for simpler agents and for testing next-step behavior.
-
-### Multi-Turn
-
-For testing conversational memory, use an array of messages:
-
-```jsonl
-{"input": ["My name is Alice", "What's my name?"], "ground_truth": "Alice"}
-```
-
-The agent receives multiple messages in sequence:
-1. Turn 1: "My name is Alice"
-2. Turn 2: "What's my name?"
-
-See the [built-in extractors](../extractors/builtin.md) for more information on how to use the agent's response from a multi-turn conversation for grading.
-
-## How It Works
-
-When you provide an array for `input`, the framework:
-1. Sends the first message to the agent
-2. Waits for the agent's response
-3. Sends the second message
-4. Continues until all messages are sent
-5. Extracts and grades the agent's response using the specified extractor and grader.
-
-## Use Cases
-
-### Testing Memory Persistence
-
-```jsonl
-{"input": ["I live in Paris", "Where do I live?"], "ground_truth": "Paris"}
-```
-
-Tests whether the agent stores information correctly using the `memory_block` extractor.
-
-### Testing Tool Call Sequences
-
-```jsonl
-{"input": ["Search for pandas", "What did you find about their diet?"], "ground_truth": "bamboo"}
-```
-
-Verifies the agent calls tools in the right order and uses results appropriately.
-
-### Testing Context Retention
-
-```jsonl
-{"input": ["My favorite color is blue", "What color do I prefer?"], "ground_truth": "blue"}
-```
-
-Ensures the agent recalls details from earlier in the conversation.
-
-### Testing Long-Term Memory
-
-```jsonl
-{"input": ["My name is Alice", "Tell me a joke", "What's my name again?"], "ground_truth": "Alice"}
-```
-
-Checks if the agent remembers information even after intervening exchanges.
-
-## Example Configuration
-
-```yaml
-name: multi-turn-test
-dataset: conversations.jsonl
-
-target:
- kind: agent
- agent_file: agent.af
- base_url: http://localhost:8283
-
-graders:
- recall:
- kind: tool
- function: contains
- extractor: last_assistant
-
-gate:
- metric_key: recall
- op: gte
- value: 0.8
-```
-
-The grader evaluates the agent's final response (after all turns).
-
-## Testing Both Response and Memory
-
-Multi-turn evaluations become especially powerful when combined with the `memory_block` extractor:
-
-```yaml
-graders:
- response_accuracy:
- kind: tool
- function: contains
- extractor: last_assistant
-
- memory_storage:
- kind: tool
- function: contains
- extractor: memory_block
- extractor_config:
- block_label: human
-```
-
-This tests two things:
-1. **Did the agent respond correctly?** (using conversation context)
-2. **Did the agent persist the information?** (to its memory blocks)
-
-An agent might pass the first test by keeping information in working memory, but fail the second by not properly storing it for long-term recall.
-
-## Context vs Persistence
-
-Consider this result:
-
-```
-Results by metric:
- response_accuracy - Avg: 1.00, Pass: 100.0%
- memory_storage - Avg: 0.00, Pass: 0.0%
-```
-
-The agent answered correctly (100%) but didn't store anything in memory (0%). This reveals important agent behavior:
-
-- **Working memory**: Agent kept information in conversation context
-- **Persistent memory**: Agent didn't update its memory blocks
-
-For short conversations, working memory is sufficient. For long-term interactions, persistent memory is crucial.
-
-## Complete Example
-
-See [`examples/multi-turn-memory/`](https://github.com/letta-ai/letta-evals/tree/main/examples/multi-turn-memory) for a working example that demonstrates:
-- Multi-turn conversation format
-- Dual metric evaluation (response + memory)
-- The difference between context-based recall and true persistence
-
-## Best Practices
-
-### 1. Keep Turns Focused
-
-Each turn should test one aspect of memory or context:
-
-```jsonl
-{"input": ["I'm allergic to peanuts", "Can I eat this cookie?"], "ground_truth": "peanut"}
-```
-
-### 2. Test Realistic Scenarios
-
-Design conversations that mirror real user interactions:
-
-```jsonl
-{"input": ["Set a reminder for tomorrow at 2pm", "What reminders do I have?"], "ground_truth": "2pm"}
-```
-
-### 3. Use Tags for Organization
-
-Tag multi-turn samples to distinguish them:
-
-```jsonl
-{"input": ["Hello", "How are you?"], "tags": ["multi-turn", "greeting"]}
-```
-
-### 4. Test Memory Limits
-
-See how far back agents can recall:
-
-```jsonl
-{"input": ["My name is Alice", "message 2", "message 3", "message 4", "What's my name?"], "ground_truth": "Alice"}
-```
-
-### 5. Combine with Memory Extractors
-
-Always verify both response and internal state for memory tests.
-
-## Limitations
-
-### Turn Count
-
-Very long conversations may exceed context windows. Monitor token usage for conversations with many turns.
-
-### State Isolation
-
-Each sample starts with a fresh agent (or fresh conversation if using `agent_id`). Multi-turn tests memory within a single conversation, not across separate conversations.
-
-### Extraction
-
-Most extractors work on the final state. If you need to check intermediate turns, consider using custom extractors.
-
-## Next Steps
-
-- [Built-in Extractors](../extractors/builtin.md) - Using memory_block extractor
-- [Custom Extractors](../extractors/custom.md) - Build extractors for complex scenarios
-- [Multi-Metric Evaluation](../graders/multi-metric.md) - Combine multiple checks
diff --git a/fern/pages/evals/assets/evaluation-progress.png b/fern/pages/evals/assets/evaluation-progress.png
deleted file mode 100644
index edd016b9..00000000
Binary files a/fern/pages/evals/assets/evaluation-progress.png and /dev/null differ
diff --git a/fern/pages/evals/cli/commands.md b/fern/pages/evals/cli/commands.md
deleted file mode 100644
index 780033f2..00000000
--- a/fern/pages/evals/cli/commands.md
+++ /dev/null
@@ -1,389 +0,0 @@
-# CLI Commands
-
-The **letta-evals** command-line interface lets you run evaluations, validate configurations, and inspect available components.
-
-**Quick overview:**
-- **`run`** - Execute an evaluation suite (most common)
-- **`validate`** - Check suite configuration without running
-- **`list-extractors`** - Show available extractors
-- **`list-graders`** - Show available grader functions
-- **Exit codes** - 0 for pass, 1 for fail (perfect for CI/CD)
-
-**Typical workflow:**
-1. Validate your suite: `letta-evals validate suite.yaml`
-2. Run evaluation: `letta-evals run suite.yaml --output results/`
-3. Check exit code: `echo $?` (0 = passed, 1 = failed)
-
-Letta Evals provides a command-line interface for running evaluations and managing configurations.
-
-## run
-
-Run an evaluation suite.
-
-```bash
-letta-evals run [options]
-```
-
-### Arguments
-
-- `suite.yaml`: Path to the suite configuration file (required)
-
-### Options
-
-#### --output, -o
-Save results to a directory.
-
-```bash
-letta-evals run suite.yaml --output results/
-```
-
-Creates:
-- `results/header.json`: Evaluation metadata
-- `results/summary.json`: Aggregate metrics and configuration
-- `results/results.jsonl`: Per-sample results (one JSON per line)
-
-#### --quiet, -q
-Quiet mode - only show pass/fail result.
-
-```bash
-letta-evals run suite.yaml --quiet
-```
-
-Output:
-```
-✓ PASSED
-```
-
-#### --max-concurrent
-Maximum concurrent sample evaluations.
-
-```bash
-letta-evals run suite.yaml --max-concurrent 10
-```
-
-Default: 15
-
-Higher values = faster evaluation but more resource usage.
-
-#### --api-key
-Letta API key (overrides LETTA_API_KEY environment variable).
-
-```bash
-letta-evals run suite.yaml --api-key your-key
-```
-
-#### --base-url
-Letta server base URL (overrides suite config and environment variable).
-
-```bash
-letta-evals run suite.yaml --base-url http://localhost:8283
-```
-
-#### --project-id
-Letta project ID for cloud deployments.
-
-```bash
-letta-evals run suite.yaml --project-id proj_abc123
-```
-
-#### --cached, -c
-Path to cached results (JSONL) for re-grading trajectories without re-running the agent.
-
-```bash
-letta-evals run suite.yaml --cached previous_results.jsonl
-```
-
-Use this to test different graders on the same agent trajectories.
-
-#### --num-runs
-Run the evaluation multiple times to measure consistency and get aggregate statistics.
-
-```bash
-letta-evals run suite.yaml --num-runs 10
-```
-
-Default: 1 (single run)
-
-**Output with multiple runs:**
-- Each run creates a separate `run_N/` directory with individual results
-- An `aggregate_stats.json` file contains statistics across all runs (mean, standard deviation, pass rate)
-
-**Use cases:**
-- Measuring consistency of non-deterministic agents
-- Getting confidence intervals for evaluation metrics
-- Testing agent variability across multiple runs
-
-See [Results - Multiple Runs](../results/overview.md#multiple-runs-statistics) for details on the statistics output.
-
-### Examples
-
-Basic run:
-```bash
-letta-evals run suite.yaml # Run evaluation, show results in terminal
-```
-
-Save results:
-```bash
-letta-evals run suite.yaml --output evaluation-results/ # Save to directory
-```
-
-High concurrency:
-```bash
-letta-evals run suite.yaml --max-concurrent 20 # Run 20 samples in parallel
-```
-
-Letta Cloud:
-```bash
-letta-evals run suite.yaml \
- --base-url https://api.letta.com \ # Cloud endpoint
- --api-key $LETTA_API_KEY \ # Your API key
- --project-id proj_abc123 # Your project
-```
-
-Quiet CI mode:
-```bash
-letta-evals run suite.yaml --quiet # Only show pass/fail
-if [ $? -eq 0 ]; then # Check exit code
- echo "Evaluation passed"
-else
- echo "Evaluation failed"
- exit 1 # Fail the CI build
-fi
-```
-
-Multiple runs with statistics:
-```bash
-letta-evals run suite.yaml --num-runs 10 --output results/
-# Creates results/run_1/, results/run_2/, ..., results/run_10/
-# Plus results/aggregate_stats.json with mean, stddev, and pass rate
-```
-
-### Exit Codes
-
-- `0`: Evaluation passed (gate criteria met)
-- `1`: Evaluation failed (gate criteria not met or error)
-
-## validate
-
-Validate a suite configuration without running it.
-
-```bash
-letta-evals validate
-```
-
-Checks:
-- YAML syntax is valid
-- Required fields are present
-- Paths exist
-- Configuration is consistent
-- Grader/extractor combinations are valid
-
-### Examples
-
-```bash
-letta-evals validate suite.yaml
-```
-
-Output on success:
-```
-✓ Suite configuration is valid
-```
-
-Output on error:
-```
-✗ Validation failed:
- - Agent file not found: agent.af
- - Grader 'my_metric' references unknown function
-```
-
-## list-extractors
-
-List all available extractors.
-
-```bash
-letta-evals list-extractors
-```
-
-Shows:
-- Built-in extractors
-- Custom extractors (if registered)
-- Brief description of each
-
-Output:
-```
-Available extractors:
- last_assistant - Extract the last assistant message
- first_assistant - Extract the first assistant message
- all_assistant - Concatenate all assistant messages
- pattern - Extract content matching regex
- tool_arguments - Extract tool call arguments
- tool_output - Extract tool return value
- after_marker - Extract content after a marker
- memory_block - Extract from memory block (requires agent_state)
-```
-
-## list-graders
-
-List all available grader functions.
-
-```bash
-letta-evals list-graders
-```
-
-Shows:
-- Built-in tool graders
-- Custom graders (if registered)
-- Brief description of each
-
-Output:
-```
-Available graders:
- exact_match - Exact string match with ground_truth
- contains - Check if contains ground_truth
- regex_match - Match regex pattern
- ascii_printable_only - Validate ASCII-only content
-```
-
-## help
-
-Show help information.
-
-```bash
-letta-evals --help
-```
-
-Show help for a specific command:
-
-```bash
-letta-evals run --help
-letta-evals validate --help
-```
-
-## Environment Variables
-
-These environment variables affect CLI behavior:
-
-### LETTA_API_KEY
-API key for Letta authentication.
-
-```bash
-export LETTA_API_KEY=your-key-here
-```
-
-### LETTA_BASE_URL
-Letta server base URL.
-
-```bash
-export LETTA_BASE_URL=http://localhost:8283
-```
-
-### LETTA_PROJECT_ID
-Letta project ID (for cloud).
-
-```bash
-export LETTA_PROJECT_ID=proj_abc123
-```
-
-### OPENAI_API_KEY
-OpenAI API key (for rubric graders).
-
-```bash
-export OPENAI_API_KEY=your-openai-key
-```
-
-### OPENAI_BASE_URL
-Custom OpenAI-compatible endpoint (optional).
-
-```bash
-export OPENAI_BASE_URL=https://your-endpoint.com/v1
-```
-
-## Configuration Priority
-
-Configuration values are resolved in this order (highest to lowest priority):
-
-1. CLI arguments (`--api-key`, `--base-url`, `--project-id`)
-2. Suite YAML configuration
-3. Environment variables
-
-## Using in CI/CD
-
-### GitHub Actions
-
-```yaml
-name: Run Evals
-on: [push]
-
-jobs:
- evaluate:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
-
- - name: Install dependencies
- run: pip install letta-evals
-
- - name: Run evaluation
- env:
- LETTA_API_KEY: ${{ secrets.LETTA_API_KEY }}
- OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- run: |
- letta-evals run suite.yaml --quiet --output results/
-
- - name: Upload results
- uses: actions/upload-artifact@v2
- with:
- name: eval-results
- path: results/
-```
-
-### GitLab CI
-
-```yaml
-evaluate:
- script:
- - pip install letta-evals
- - letta-evals run suite.yaml --quiet --output results/
- artifacts:
- paths:
- - results/
- variables:
- LETTA_API_KEY: $LETTA_API_KEY
- OPENAI_API_KEY: $OPENAI_API_KEY
-```
-
-## Debugging
-
-### Verbose Output
-
-Currently, the CLI uses standard verbosity. For debugging:
-
-1. Check the output directory for detailed results
-2. Examine `summary.json` for aggregate metrics
-3. Check `results.jsonl` for per-sample details
-
-### Common Issues
-
-**"Agent file not found"**
-```bash
-# Check file exists relative to suite YAML location
-ls -la path/to/agent.af
-```
-
-**"Connection refused"**
-```bash
-# Verify Letta server is running
-curl http://localhost:8283/v1/health
-```
-
-**"Invalid API key"**
-```bash
-# Check environment variable is set
-echo $LETTA_API_KEY
-```
-
-## Next Steps
-
-- [Understanding Results](../results/overview.md) - Interpreting evaluation output
-- [Suite YAML Reference](../configuration/suite-yaml.md) - Complete configuration options
-- [Getting Started](../getting-started.md) - Complete tutorial with examples
diff --git a/fern/pages/evals/cli/commands.mdx b/fern/pages/evals/cli/commands.mdx
deleted file mode 100644
index 42b6b9a1..00000000
--- a/fern/pages/evals/cli/commands.mdx
+++ /dev/null
@@ -1,342 +0,0 @@
-# CLI Commands
-
-The **letta-evals** command-line interface lets you run evaluations, validate configurations, and inspect available components.
-
-
-**Quick overview:**
-- **`run`** - Execute an evaluation suite (most common)
-- **`validate`** - Check suite configuration without running
-- **`list-extractors`** - Show available extractors
-- **`list-graders`** - Show available grader functions
-- **Exit codes** - 0 for pass, 1 for fail (perfect for CI/CD)
-
-
-**Typical workflow:**
-1. Validate your suite: `letta-evals validate suite.yaml`
-2. Run evaluation: `letta-evals run suite.yaml --output results/`
-3. Check exit code: `echo $?` (0 = passed, 1 = failed)
-
-## run
-
-Run an evaluation suite.
-
-```bash
-letta-evals run [options]
-```
-
-### Arguments
-
-- `suite.yaml`: Path to the suite configuration file (required)
-
-### Options
-
-#### --output, -o
-Save results to a directory.
-
-```bash
-letta-evals run suite.yaml --output results/
-```
-
-Creates:
-- `results/header.json`: Evaluation metadata
-- `results/summary.json`: Aggregate metrics and configuration
-- `results/results.jsonl`: Per-sample results (one JSON per line)
-
-#### --quiet, -q
-Quiet mode - only show pass/fail result.
-
-```bash
-letta-evals run suite.yaml --quiet
-```
-
-Output:
-```
-✓ PASSED
-```
-
-#### --max-concurrent
-Maximum concurrent sample evaluations. **Default**: 15
-
-```bash
-letta-evals run suite.yaml --max-concurrent 10
-```
-
-Higher values = faster evaluation but more resource usage.
-
-#### --api-key
-Letta API key (overrides LETTA_API_KEY environment variable).
-
-```bash
-letta-evals run suite.yaml --api-key your-key
-```
-
-#### --base-url
-Letta server base URL (overrides suite config and environment variable).
-
-```bash
-letta-evals run suite.yaml --base-url https://api.letta.com
-```
-
-#### --project-id
-Letta project ID for cloud deployments.
-
-```bash
-letta-evals run suite.yaml --project-id proj_abc123
-```
-
-#### --cached, -c
-Path to cached results (JSONL) for re-grading trajectories without re-running the agent.
-
-```bash
-letta-evals run suite.yaml --cached previous_results.jsonl
-```
-
-Use this to test different graders on the same agent trajectories.
-
-#### --num-runs
-Run the evaluation multiple times to measure consistency. **Default**: 1
-
-```bash
-letta-evals run suite.yaml --num-runs 10
-```
-
-**Output with multiple runs:**
-- Each run creates a separate `run_N/` directory with individual results
-- An `aggregate_stats.json` file contains statistics across all runs (mean, standard deviation, pass rate)
-
-### Examples
-
-Basic run:
-```bash
-letta-evals run suite.yaml # Run evaluation, show results in terminal
-```
-
-Save results:
-```bash
-letta-evals run suite.yaml --output evaluation-results/ # Save to directory
-```
-
-Letta Cloud:
-```bash
-letta-evals run suite.yaml \
- --base-url https://api.letta.com \
- --api-key $LETTA_API_KEY \
- --project-id proj_abc123
-```
-
-Quiet CI mode:
-```bash
-letta-evals run suite.yaml --quiet
-if [ $? -eq 0 ]; then
- echo "Evaluation passed"
-else
- echo "Evaluation failed"
- exit 1
-fi
-```
-
-### Exit Codes
-
-- `0`: Evaluation passed (gate criteria met)
-- `1`: Evaluation failed (gate criteria not met or error)
-
-## validate
-
-Validate a suite configuration without running it.
-
-```bash
-letta-evals validate
-```
-
-Checks:
-- YAML syntax is valid
-- Required fields are present
-- Paths exist
-- Configuration is consistent
-- Grader/extractor combinations are valid
-
-Output on success:
-```
-✓ Suite configuration is valid
-```
-
-Output on error:
-```
-✗ Validation failed:
- - Agent file not found: agent.af
- - Grader 'my_metric' references unknown function
-```
-
-## list-extractors
-
-List all available extractors.
-
-```bash
-letta-evals list-extractors
-```
-
-Output:
-```
-Available extractors:
- last_assistant - Extract the last assistant message
- first_assistant - Extract the first assistant message
- all_assistant - Concatenate all assistant messages
- pattern - Extract content matching regex
- tool_arguments - Extract tool call arguments
- tool_output - Extract tool return value
- after_marker - Extract content after a marker
- memory_block - Extract from memory block (requires agent_state)
-```
-
-## list-graders
-
-List all available grader functions.
-
-```bash
-letta-evals list-graders
-```
-
-Output:
-```
-Available graders:
- exact_match - Exact string match with ground_truth
- contains - Check if contains ground_truth
- regex_match - Match regex pattern
- ascii_printable_only - Validate ASCII-only content
-```
-
-## help
-
-Show help information.
-
-```bash
-letta-evals --help
-```
-
-Show help for a specific command:
-
-```bash
-letta-evals run --help
-letta-evals validate --help
-```
-
-## Environment Variables
-
-### LETTA_API_KEY
-API key for Letta authentication.
-
-```bash
-export LETTA_API_KEY=your-key-here
-```
-
-### LETTA_BASE_URL
-Letta server base URL.
-
-```bash
-export LETTA_BASE_URL=https://api.letta.com
-```
-
-### LETTA_PROJECT_ID
-Letta project ID (for cloud).
-
-```bash
-export LETTA_PROJECT_ID=proj_abc123
-```
-
-### OPENAI_API_KEY
-OpenAI API key (for rubric graders).
-
-```bash
-export OPENAI_API_KEY=your-openai-key
-```
-
-## Configuration Priority
-
-Configuration values are resolved in this order (highest to lowest priority):
-
-1. CLI arguments (`--api-key`, `--base-url`, `--project-id`)
-2. Suite YAML configuration
-3. Environment variables
-
-## Using in CI/CD
-
-### GitHub Actions
-
-```yaml
-name: Run Evals
-on: [push]
-
-jobs:
- evaluate:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
-
- - name: Install dependencies
- run: pip install letta-evals
-
- - name: Run evaluation
- env:
- LETTA_API_KEY: ${{ secrets.LETTA_API_KEY }}
- OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- run: |
- letta-evals run suite.yaml --quiet --output results/
-
- - name: Upload results
- uses: actions/upload-artifact@v2
- with:
- name: eval-results
- path: results/
-```
-
-### GitLab CI
-
-```yaml
-evaluate:
- script:
- - pip install letta-evals
- - letta-evals run suite.yaml --quiet --output results/
- artifacts:
- paths:
- - results/
- variables:
- LETTA_API_KEY: $LETTA_API_KEY
- OPENAI_API_KEY: $OPENAI_API_KEY
-```
-
-## Debugging
-
-### Common Issues
-
-
-**"Agent file not found"**
-
-```bash
-# Check file exists relative to suite YAML location
-ls -la path/to/agent.af
-```
-
-
-
-**"Connection refused"**
-
-```bash
-# Verify Letta server is running
-curl https://api.letta.com/v1/health
-```
-
-
-
-**"Invalid API key"**
-
-```bash
-# Check environment variable is set
-echo $LETTA_API_KEY
-```
-
-
-## Next Steps
-
-- [Understanding Results](/evals/results-metrics/understanding-results) - Interpreting evaluation output
-- [Suite YAML Reference](/evals/configuration/suite-yaml-reference) - Complete configuration options
-- [Getting Started](/evals/get-started/getting-started) - Complete tutorial with examples
diff --git a/fern/pages/evals/concepts/datasets.md b/fern/pages/evals/concepts/datasets.md
deleted file mode 100644
index 5b5232bb..00000000
--- a/fern/pages/evals/concepts/datasets.md
+++ /dev/null
@@ -1,418 +0,0 @@
-# Datasets
-
-**Datasets** are the test cases that define what your agent will be evaluated on. Each sample in your dataset represents one evaluation scenario.
-
-**Quick overview:**
-- **Two formats**: JSONL (flexible, powerful) or CSV (simple, spreadsheet-friendly)
-- **Required field**: `input` - the prompt(s) to send to the agent
-- **Common fields**: `ground_truth` (expected answer), `tags` (for filtering), `metadata` (extra info)
-- **Advanced fields**: `agent_args` (customize agent per sample), `rubric_vars` (per-sample rubric context)
-- **Multi-turn support**: Send multiple messages in sequence using arrays
-
-**Typical workflow:**
-1. Create a JSONL or CSV file with test cases
-2. Reference it in your suite YAML: `dataset: test_cases.jsonl`
-3. Run evaluation - each sample is tested independently
-4. Results show per-sample and aggregate scores
-
-Datasets can be created in two formats: **JSONL** or **CSV**. Choose based on your team's workflow and complexity needs.
-
-## Dataset Formats
-
-### JSONL Format
-
-Each line is a JSON object representing one test case:
-
-```jsonl
-{"input": "What's the capital of France?", "ground_truth": "Paris"}
-{"input": "Calculate 2+2", "ground_truth": "4"}
-{"input": "What color is the sky?", "ground_truth": "blue"}
-```
-
-**Best for:**
-- Complex data structures (nested objects, arrays)
-- Multi-turn conversations
-- Advanced features (agent_args, rubric_vars)
-- Teams comfortable with JSON/code
-- Version control (clean line-by-line diffs)
-
-### CSV Format
-
-Standard CSV with headers:
-
-```csv
-input,ground_truth
-"What's the capital of France?","Paris"
-"Calculate 2+2","4"
-"What color is the sky?","blue"
-```
-
-**Best for:**
-- Simple question-answer pairs
-- Teams that prefer spreadsheets (Excel, Google Sheets)
-- Non-technical collaborators creating test cases
-- Quick dataset creation and editing
-- Easy sharing with non-developers
-
-## Quick Reference
-
-| Field | Required | Type | Purpose |
-|-------|----------|------|---------|
-| `input` | ✅ | string or array | Prompt(s) to send to agent |
-| `ground_truth` | ❌ | string | Expected answer (for tool graders) |
-| `tags` | ❌ | array of strings | For filtering samples |
-| `agent_args` | ❌ | object | Per-sample agent customization |
-| `rubric_vars` | ❌ | object | Per-sample rubric variables |
-| `metadata` | ❌ | object | Arbitrary extra data |
-| `id` | ❌ | integer | Sample ID (auto-assigned if omitted) |
-
-## Field Reference
-
-### Required Fields
-
-#### input
-The prompt(s) to send to the agent. Can be a string or array of strings:
-
-Single message:
-```json
-{"input": "Hello, who are you?"}
-```
-
-Multi-turn conversation:
-```json
-{"input": ["Hello", "What's your name?", "Tell me about yourself"]}
-```
-
-### Optional Fields
-
-#### ground_truth
-The expected answer or content to check against. Required for most tool graders (exact_match, contains, etc.):
-
-```json
-{"input": "What is 2+2?", "ground_truth": "4"}
-```
-
-#### metadata
-Arbitrary additional data about the sample:
-
-```json
-{
- "input": "What is photosynthesis?",
- "ground_truth": "process where plants convert light into energy",
- "metadata": {
- "category": "biology",
- "difficulty": "medium"
- }
-}
-```
-
-#### tags
-List of tags for filtering samples:
-
-```json
-{"input": "Solve x^2 = 16", "ground_truth": "4", "tags": ["math", "algebra"]}
-```
-
-Filter by tags in your suite:
-```yaml
-sample_tags: [math] # Only samples tagged "math" will be evaluated
-```
-
-#### agent_args
-
-Custom arguments passed to programmatic agent creation when using `agent_script`. Allows per-sample agent customization.
-
-JSONL:
-```json
-{
- "input": "What items do we have?",
- "agent_args": {
- "item": {"sku": "SKU-123", "name": "Widget A", "price": 19.99}
- }
-}
-```
-
-CSV:
-```csv
-input,agent_args
-"What items do we have?","{""item"": {""sku"": ""SKU-123"", ""name"": ""Widget A"", ""price"": 19.99}}"
-```
-
-Your agent factory function can access these values via `sample.agent_args` to customize agent configuration.
-
-See [Targets - agent_script](./targets.md#agent_script) for details on programmatic agent creation.
-
-#### rubric_vars
-
-Variables to inject into rubric templates when using rubric graders. This allows you to provide per-sample context or examples to the LLM judge.
-
-**Example:** Evaluating code quality against a reference implementation.
-
-JSONL:
-```jsonl
-{"input": "Write a function to calculate fibonacci numbers", "rubric_vars": {"reference_code": "def fib(n):\n if n <= 1: return n\n return fib(n-1) + fib(n-2)", "required_features": "recursion, base case"}}
-```
-
-CSV:
-```csv
-input,rubric_vars
-"Write a function to calculate fibonacci numbers","{""reference_code"": ""def fib(n):\n if n <= 1: return n\n return fib(n-1) + fib(n-2)"", ""required_features"": ""recursion, base case""}"
-```
-
-In your rubric template file, reference variables with `{variable_name}`:
-
-**rubric.txt:**
-```
-Evaluate the submitted code against this reference implementation:
-
-{reference_code}
-
-Required features: {required_features}
-
-Score on correctness (0.6) and code quality (0.4).
-```
-
-When the rubric grader runs, variables are replaced with values from `rubric_vars`:
-
-**Final formatted prompt sent to LLM:**
-```
-Evaluate the submitted code against this reference implementation:
-
-def fib(n):
- if n <= 1: return n
- return fib(n-1) + fib(n-2)
-
-Required features: recursion, base case
-
-Score on correctness (0.6) and code quality (0.4).
-```
-
-This lets you customize evaluation criteria per sample using the same rubric template.
-
-See [Rubric Graders](../graders/rubric-graders.md) for details on rubric templates.
-
-#### id
-Sample ID is automatically assigned (0-based index) if not provided. You can override:
-
-```json
-{"id": 42, "input": "Test case 42"}
-```
-
-## Complete Example
-
-```jsonl
-{"id": 1, "input": "What is the capital of France?", "ground_truth": "Paris", "tags": ["geography", "easy"], "metadata": {"region": "Europe"}}
-{"id": 2, "input": "Calculate the square root of 144", "ground_truth": "12", "tags": ["math", "medium"]}
-{"id": 3, "input": ["Hello", "What can you help me with?"], "tags": ["conversation"]}
-```
-
-## Dataset Best Practices
-
-### 1. Clear Ground Truth
-
-Make ground truth specific enough to grade but flexible enough to match valid responses:
-
-Good:
-```json
-{"input": "What's the largest planet?", "ground_truth": "Jupiter"}
-```
-
-Too strict (might miss valid answers):
-```json
-{"input": "What's the largest planet?", "ground_truth": "Jupiter is the largest planet in our solar system."}
-```
-
-### 2. Diverse Test Cases
-
-Include edge cases and variations:
-
-```jsonl
-{"input": "What is 2+2?", "ground_truth": "4", "tags": ["math", "easy"]}
-{"input": "What is 0.1 + 0.2?", "ground_truth": "0.3", "tags": ["math", "floating_point"]}
-{"input": "What is 999999999 + 1?", "ground_truth": "1000000000", "tags": ["math", "large_numbers"]}
-```
-
-### 3. Use Tags for Organization
-
-Organize samples by type, difficulty, or feature:
-
-```json
-{"tags": ["tool_usage", "search"]}
-{"tags": ["memory", "recall"]}
-{"tags": ["reasoning", "multi_step"]}
-```
-
-### 4. Multi-Turn Conversations
-
-Test conversational context:
-
-```json
-{
- "input": [
- "My name is Alice",
- "What's my name?"
- ],
- "ground_truth": "Alice",
- "tags": ["memory", "context"]
-}
-```
-
-### 5. No Ground Truth for LLM Judges
-
-If using rubric graders, ground truth is optional:
-
-```jsonl
-{"input": "Write a creative story about a robot", "tags": ["creative"]}
-{"input": "Explain quantum computing simply", "tags": ["explanation"]}
-```
-
-The LLM judge evaluates based on the rubric, not ground truth.
-
-## Loading Datasets
-
-Datasets are automatically loaded by the runner:
-
-```yaml
-dataset: path/to/dataset.jsonl # Path to your test cases (JSONL or CSV)
-```
-
-Paths are relative to the suite YAML file location.
-
-## Dataset Filtering
-
-### Limit Sample Count
-
-```yaml
-max_samples: 10 # Only evaluate first 10 samples (useful for testing)
-```
-
-### Filter by Tags
-
-```yaml
-sample_tags: [math, medium] # Only samples with ALL these tags
-```
-
-## Creating Datasets Programmatically
-
-You can generate datasets with Python:
-
-```python
-import json
-
-samples = []
-for i in range(100):
- samples.append({
- "input": f"What is {i} + {i}?",
- "ground_truth": str(i + i),
- "tags": ["math", "addition"]
- })
-
-with open("dataset.jsonl", "w") as f:
- for sample in samples:
- f.write(json.dumps(sample) + "\n")
-```
-
-## Dataset Format Validation
-
-The runner validates:
-- Each line is valid JSON
-- Required fields are present
-- Field types are correct
-
-Validation errors will be reported with line numbers.
-
-## Examples by Use Case
-
-### Question Answering
-
-JSONL:
-```jsonl
-{"input": "What is the capital of France?", "ground_truth": "Paris"}
-{"input": "Who wrote Romeo and Juliet?", "ground_truth": "Shakespeare"}
-```
-
-CSV:
-```csv
-input,ground_truth
-"What is the capital of France?","Paris"
-"Who wrote Romeo and Juliet?","Shakespeare"
-```
-
-### Tool Usage Testing
-
-JSONL:
-```jsonl
-{"input": "Search for information about pandas", "ground_truth": "search"}
-{"input": "Calculate 15 * 23", "ground_truth": "calculator"}
-```
-
-CSV:
-```csv
-input,ground_truth
-"Search for information about pandas","search"
-"Calculate 15 * 23","calculator"
-```
-
-Ground truth = expected tool name.
-
-### Memory Testing (Multi-turn)
-
-JSONL:
-```jsonl
-{"input": ["Remember that my favorite color is blue", "What's my favorite color?"], "ground_truth": "blue"}
-{"input": ["I live in Tokyo", "Where do I live?"], "ground_truth": "Tokyo"}
-```
-
-CSV (using JSON array strings):
-```csv
-input,ground_truth
-"[""Remember that my favorite color is blue"", ""What's my favorite color?""]","blue"
-"[""I live in Tokyo"", ""Where do I live?""]","Tokyo"
-```
-
-### Code Generation
-
-JSONL:
-```jsonl
-{"input": "Write a function to reverse a string in Python"}
-{"input": "Create a SQL query to find users older than 21"}
-```
-
-CSV:
-```csv
-input
-"Write a function to reverse a string in Python"
-"Create a SQL query to find users older than 21"
-```
-
-Use rubric graders to evaluate code quality.
-
-## CSV Advanced Features
-
-CSV supports all the same features as JSONL by encoding complex data as JSON strings in cells:
-
-**Multi-turn conversations** (requires escaped JSON array string):
-```csv
-input,ground_truth
-"[""Hello"", ""What's your name?""]","Alice"
-```
-
-**Agent arguments** (requires escaped JSON object string):
-```csv
-input,agent_args
-"What items do we have?","{""initial_inventory"": [""apple"", ""banana""]}"
-```
-
-**Rubric variables** (requires escaped JSON object string):
-```csv
-input,rubric_vars
-"Write a story","{""max_length"": 500, ""genre"": ""sci-fi""}"
-```
-
-**Note:** Complex data structures require JSON encoding in CSV. If you're frequently using these advanced features, JSONL may be easier to read and maintain.
-
-## Next Steps
-
-- [Suite YAML Reference](../configuration/suite-yaml.md) - Complete configuration options including filtering
-- [Graders](./graders.md) - How to evaluate agent responses
-- [Multi-Turn Conversations](../advanced/multi-turn-conversations.md) - Testing conversational flows
diff --git a/fern/pages/evals/concepts/datasets.mdx b/fern/pages/evals/concepts/datasets.mdx
deleted file mode 100644
index 20706949..00000000
--- a/fern/pages/evals/concepts/datasets.mdx
+++ /dev/null
@@ -1,425 +0,0 @@
-# Datasets
-
-**Datasets** are the test cases that define what your agent will be evaluated on. Each sample in your dataset represents one evaluation scenario.
-
-
-**Quick overview:**
-- **Two formats**: JSONL (flexible, powerful) or CSV (simple, spreadsheet-friendly)
-- **Required field**: `input` - the prompt(s) to send to the agent
-- **Common fields**: `ground_truth` (expected answer), `tags` (for filtering), `metadata` (extra info)
-- **Advanced fields**: `agent_args` (customize agent per sample), `rubric_vars` (per-sample rubric context)
-- **Multi-turn support**: Send multiple messages in sequence using arrays
-
-
-**Typical workflow:**
-1. Create a JSONL or CSV file with test cases
-2. Reference it in your suite YAML: `dataset: test_cases.jsonl`
-3. Run evaluation - each sample is tested independently
-4. Results show per-sample and aggregate scores
-
-Datasets can be created in two formats: **JSONL** or **CSV**. Choose based on your team's workflow and complexity needs.
-
-## Dataset Formats
-
-### JSONL Format
-
-Each line is a JSON object representing one test case:
-
-```jsonl
-{"input": "What's the capital of France?", "ground_truth": "Paris"}
-{"input": "Calculate 2+2", "ground_truth": "4"}
-{"input": "What color is the sky?", "ground_truth": "blue"}
-```
-
-**Best for:**
-- Complex data structures (nested objects, arrays)
-- Multi-turn conversations
-- Advanced features (agent_args, rubric_vars)
-- Teams comfortable with JSON/code
-- Version control (clean line-by-line diffs)
-
-### CSV Format
-
-Standard CSV with headers:
-
-```csv
-input,ground_truth
-"What's the capital of France?","Paris"
-"Calculate 2+2","4"
-"What color is the sky?","blue"
-```
-
-**Best for:**
-- Simple question-answer pairs
-- Teams that prefer spreadsheets (Excel, Google Sheets)
-- Non-technical collaborators creating test cases
-- Quick dataset creation and editing
-- Easy sharing with non-developers
-
-## Quick Reference
-
-| Field | Required | Type | Purpose |
-|-------|----------|------|---------|
-| `input` | ✅ | string or array | Prompt(s) to send to agent |
-| `ground_truth` | ❌ | string | Expected answer (for tool graders) |
-| `tags` | ❌ | array of strings | For filtering samples |
-| `agent_args` | ❌ | object | Per-sample agent customization |
-| `rubric_vars` | ❌ | object | Per-sample rubric variables |
-| `metadata` | ❌ | object | Arbitrary extra data |
-| `id` | ❌ | integer | Sample ID (auto-assigned if omitted) |
-
-## Field Reference
-
-### Required Fields
-
-#### input
-The prompt(s) to send to the agent. Can be a string or array of strings:
-
-Single message:
-```json
-{"input": "Hello, who are you?"}
-```
-
-Multi-turn conversation:
-```json
-{"input": ["Hello", "What's your name?", "Tell me about yourself"]}
-```
-
-### Optional Fields
-
-#### ground_truth
-The expected answer or content to check against. Required for most tool graders (exact_match, contains, etc.):
-
-```json
-{"input": "What is 2+2?", "ground_truth": "4"}
-```
-
-#### metadata
-Arbitrary additional data about the sample:
-
-```json
-{
- "input": "What is photosynthesis?",
- "ground_truth": "process where plants convert light into energy",
- "metadata": {
- "category": "biology",
- "difficulty": "medium"
- }
-}
-```
-
-#### tags
-List of tags for filtering samples:
-
-```json
-{"input": "Solve x^2 = 16", "ground_truth": "4", "tags": ["math", "algebra"]}
-```
-
-Filter by tags in your suite:
-```yaml
-sample_tags: [math] # Only samples tagged "math" will be evaluated
-```
-
-#### agent_args
-
-Custom arguments passed to programmatic agent creation when using `agent_script`. Allows per-sample agent customization.
-
-JSONL:
-```json
-{
- "input": "What items do we have?",
- "agent_args": {
- "item": {"sku": "SKU-123", "name": "Widget A", "price": 19.99}
- }
-}
-```
-
-CSV:
-```csv
-input,agent_args
-"What items do we have?","{""item"": {""sku"": ""SKU-123"", ""name"": ""Widget A"", ""price"": 19.99}}"
-```
-
-Your agent factory function can access these values via `sample.agent_args` to customize agent configuration.
-
-See [Targets - agent_script](/guides/evals/concepts/targets#agent_script) for details on programmatic agent creation.
-
-#### rubric_vars
-
-Variables to inject into rubric templates when using rubric graders. This allows you to provide per-sample context or examples to the LLM judge.
-
-**Example:** Evaluating code quality against a reference implementation.
-
-JSONL:
-```jsonl
-{"input": "Write a function to calculate fibonacci numbers", "rubric_vars": {"reference_code": "def fib(n):\n if n <= 1: return n\n return fib(n-1) + fib(n-2)", "required_features": "recursion, base case"}}
-```
-
-CSV:
-```csv
-input,rubric_vars
-"Write a function to calculate fibonacci numbers","{""reference_code"": ""def fib(n):\n if n <= 1: return n\n return fib(n-1) + fib(n-2)"", ""required_features"": ""recursion, base case""}"
-```
-
-In your rubric template file, reference variables with `{variable_name}`:
-
-**rubric.txt:**
-```
-Evaluate the submitted code against this reference implementation:
-
-{reference_code}
-
-Required features: {required_features}
-
-Score on correctness (0.6) and code quality (0.4).
-```
-
-When the rubric grader runs, variables are replaced with values from `rubric_vars`:
-
-**Final formatted prompt sent to LLM:**
-```
-Evaluate the submitted code against this reference implementation:
-
-def fib(n):
- if n <= 1: return n
- return fib(n-1) + fib(n-2)
-
-Required features: recursion, base case
-
-Score on correctness (0.6) and code quality (0.4).
-```
-
-This lets you customize evaluation criteria per sample using the same rubric template.
-
-See [Rubric Graders](/guides/evals/graders/rubric-graders) for details on rubric templates.
-
-#### id
-Sample ID is automatically assigned (0-based index) if not provided. You can override:
-
-```json
-{"id": 42, "input": "Test case 42"}
-```
-
-## Complete Example
-
-```jsonl
-{"id": 1, "input": "What is the capital of France?", "ground_truth": "Paris", "tags": ["geography", "easy"], "metadata": {"region": "Europe"}}
-{"id": 2, "input": "Calculate the square root of 144", "ground_truth": "12", "tags": ["math", "medium"]}
-{"id": 3, "input": ["Hello", "What can you help me with?"], "tags": ["conversation"]}
-```
-
-## Dataset Best Practices
-
-### 1. Clear Ground Truth
-
-Make ground truth specific enough to grade but flexible enough to match valid responses:
-
-
-Good:
-```json
-{"input": "What's the largest planet?", "ground_truth": "Jupiter"}
-```
-
-
-
-Too strict (might miss valid answers):
-```json
-{"input": "What's the largest planet?", "ground_truth": "Jupiter is the largest planet in our solar system."}
-```
-
-
-### 2. Diverse Test Cases
-
-Include edge cases and variations:
-
-```jsonl
-{"input": "What is 2+2?", "ground_truth": "4", "tags": ["math", "easy"]}
-{"input": "What is 0.1 + 0.2?", "ground_truth": "0.3", "tags": ["math", "floating_point"]}
-{"input": "What is 999999999 + 1?", "ground_truth": "1000000000", "tags": ["math", "large_numbers"]}
-```
-
-### 3. Use Tags for Organization
-
-Organize samples by type, difficulty, or feature:
-
-```json
-{"tags": ["tool_usage", "search"]}
-{"tags": ["memory", "recall"]}
-{"tags": ["reasoning", "multi_step"]}
-```
-
-### 4. Multi-Turn Conversations
-
-Test conversational context and memory updates:
-
-```jsonl
-{"input": ["My name is Alice", "What's my name?"], "ground_truth": "Alice", "tags": ["memory", "recall"]}
-{"input": ["Please remember that I like bananas.", "Actually, sorry, I meant I like apples."], "ground_truth": "apples", "tags": ["memory", "correction"]}
-{"input": ["I work at Google", "Update my workplace to Microsoft", "Where do I work?"], "ground_truth": "Microsoft", "tags": ["memory", "multi_step"]}
-```
-
-
-**Testing memory corrections:** Use multi-turn inputs to test if agents properly update memory when users correct themselves. Combine with the `memory_block` extractor to verify the final memory state, not just the response.
-
-
-### 5. No Ground Truth for LLM Judges
-
-If using rubric graders, ground truth is optional:
-
-```jsonl
-{"input": "Write a creative story about a robot", "tags": ["creative"]}
-{"input": "Explain quantum computing simply", "tags": ["explanation"]}
-```
-
-The LLM judge evaluates based on the rubric, not ground truth.
-
-## Loading Datasets
-
-Datasets are automatically loaded by the runner:
-
-```yaml
-dataset: path/to/dataset.jsonl # Path to your test cases (JSONL or CSV)
-```
-
-Paths are relative to the suite YAML file location.
-
-## Dataset Filtering
-
-### Limit Sample Count
-
-```yaml
-max_samples: 10 # Only evaluate first 10 samples (useful for testing)
-```
-
-### Filter by Tags
-
-```yaml
-sample_tags: [math, medium] # Only samples with ALL these tags
-```
-
-## Creating Datasets Programmatically
-
-You can generate datasets with Python:
-
-```python
-import json
-
-samples = []
-for i in range(100):
- samples.append({
- "input": f"What is {i} + {i}?",
- "ground_truth": str(i + i),
- "tags": ["math", "addition"]
- })
-
-with open("dataset.jsonl", "w") as f:
- for sample in samples:
- f.write(json.dumps(sample) + "\n")
-```
-
-## Dataset Format Validation
-
-The runner validates:
-- Each line is valid JSON
-- Required fields are present
-- Field types are correct
-
-Validation errors will be reported with line numbers.
-
-## Examples by Use Case
-
-### Question Answering
-
-JSONL:
-```jsonl
-{"input": "What is the capital of France?", "ground_truth": "Paris"}
-{"input": "Who wrote Romeo and Juliet?", "ground_truth": "Shakespeare"}
-```
-
-CSV:
-```csv
-input,ground_truth
-"What is the capital of France?","Paris"
-"Who wrote Romeo and Juliet?","Shakespeare"
-```
-
-### Tool Usage Testing
-
-JSONL:
-```jsonl
-{"input": "Search for information about pandas", "ground_truth": "search"}
-{"input": "Calculate 15 * 23", "ground_truth": "calculator"}
-```
-
-CSV:
-```csv
-input,ground_truth
-"Search for information about pandas","search"
-"Calculate 15 * 23","calculator"
-```
-
-Ground truth = expected tool name.
-
-### Memory Testing (Multi-turn)
-
-JSONL:
-```jsonl
-{"input": ["Remember that my favorite color is blue", "What's my favorite color?"], "ground_truth": "blue"}
-{"input": ["I live in Tokyo", "Where do I live?"], "ground_truth": "Tokyo"}
-```
-
-CSV (using JSON array strings):
-```csv
-input,ground_truth
-"[""Remember that my favorite color is blue"", ""What's my favorite color?""]","blue"
-"[""I live in Tokyo"", ""Where do I live?""]","Tokyo"
-```
-
-### Code Generation
-
-JSONL:
-```jsonl
-{"input": "Write a function to reverse a string in Python"}
-{"input": "Create a SQL query to find users older than 21"}
-```
-
-CSV:
-```csv
-input
-"Write a function to reverse a string in Python"
-"Create a SQL query to find users older than 21"
-```
-
-Use rubric graders to evaluate code quality.
-
-## CSV Advanced Features
-
-CSV supports all the same features as JSONL by encoding complex data as JSON strings in cells:
-
-**Multi-turn conversations** (requires escaped JSON array string):
-```csv
-input,ground_truth
-"[""Hello"", ""What's your name?""]","Alice"
-```
-
-**Agent arguments** (requires escaped JSON object string):
-```csv
-input,agent_args
-"What items do we have?","{""initial_inventory"": [""apple"", ""banana""]}"
-```
-
-**Rubric variables** (requires escaped JSON object string):
-```csv
-input,rubric_vars
-"Write a story","{""max_length"": 500, ""genre"": ""sci-fi""}"
-```
-
-
-**Note:** Complex data structures require JSON encoding in CSV. If you're frequently using these advanced features, JSONL may be easier to read and maintain.
-
-
-## Next Steps
-
-- [Suite YAML Reference](/guides/evals/configuration/suite-yaml) - Complete configuration options including filtering
-- [Graders](/guides/evals/concepts/graders) - How to evaluate agent responses
-- [Multi-Turn Conversations](/guides/evals/advanced/multi-turn-conversations) - Testing conversational flows
diff --git a/fern/pages/evals/concepts/extractors.md b/fern/pages/evals/concepts/extractors.md
deleted file mode 100644
index c8593247..00000000
--- a/fern/pages/evals/concepts/extractors.md
+++ /dev/null
@@ -1,394 +0,0 @@
-# Extractors
-
-**Extractors** select what content to evaluate from an agent's response. They navigate the conversation trajectory and extract the specific piece you want to grade.
-
-**Quick overview:**
-- **Purpose**: Agent responses are complex (messages, tool calls, memory) - extractors isolate what to grade
-- **Built-in options**: last_assistant, tool_arguments, memory_block, pattern, and more
-- **Flexible**: Different graders can use different extractors in the same suite
-- **Automatic**: No setup needed - just specify in your grader config
-
-**Common patterns:**
-- `last_assistant` - Most common, gets the agent's final message (90% of use cases)
-- `tool_arguments` - Verify agent called the right tool with correct args
-- `memory_block` - Check if agent updated memory correctly
-- `pattern` - Extract structured data with regex
-
-Extractors determine what part of the agent's response gets graded. They pull out specific content from the conversation trajectory.
-
-## Why Extractors?
-
-An agent's response is complex - it includes assistant messages, tool calls, tool returns, memory updates, etc. Extractors let you focus on exactly what you want to evaluate.
-
-**The evaluation flow:**
-```
-Agent Response → Extractor → Submission Text → Grader → Score
-```
-
-For example:
-```
-Full trajectory:
- UserMessage: "What's the capital of France?"
- ToolCallMessage: search(query="capital of france")
- ToolReturnMessage: "Paris is the capital..."
- AssistantMessage: "The capital of France is Paris."
-
-↓ extractor: last_assistant ↓
-
-Extracted: "The capital of France is Paris."
-
-↓ grader: contains (ground_truth="Paris") ↓
-
-Score: 1.0
-```
-
-## Trajectory Structure
-
-A trajectory is a list of turns, where each turn is a list of Letta messages:
-
-```python
-[
- [UserMessage(...), AssistantMessage(...), ToolCallMessage(...), ToolReturnMessage(...)], # Turn 1
- [AssistantMessage(...)] # Turn 2
-]
-```
-
-Extractors navigate this structure to pull out the submission text.
-
-## Built-in Extractors
-
-### last_assistant
-
-Extracts the last assistant message content.
-
-```yaml
-graders:
- quality:
- kind: tool
- function: contains
- extractor: last_assistant # Extract final agent message
-```
-
-Most common extractor - gets the agent's final response.
-
-### first_assistant
-
-Extracts the first assistant message content.
-
-```yaml
-graders:
- initial_response:
- kind: tool
- function: contains
- extractor: first_assistant # Extract first agent message
-```
-
-Useful for testing immediate responses before tool usage.
-
-### all_assistant
-
-Concatenates all assistant messages with a separator.
-
-```yaml
-graders:
- complete_response:
- kind: rubric
- prompt_path: rubric.txt
- extractor: all_assistant # Concatenate all agent messages
- extractor_config:
- separator: "\n\n" # Join messages with double newline
-```
-
-Use when you need the full conversation context.
-
-### last_turn
-
-Extracts all assistant messages from the last turn only.
-
-```yaml
-graders:
- final_turn:
- kind: tool
- function: contains
- extractor: last_turn # Messages from final turn only
- extractor_config:
- separator: " " # Join with spaces
-```
-
-Useful when the agent makes multiple statements in the final turn.
-
-### pattern
-
-Extracts content matching a regex pattern from assistant messages.
-
-```yaml
-graders:
- extract_number:
- kind: tool
- function: exact_match
- extractor: pattern # Extract using regex
- extractor_config:
- pattern: 'Result: (\d+)' # Regex pattern to match
- group: 1 # Extract capture group 1
- search_all: false # Only find first match
-```
-
-Example: Extract "42" from "The answer is Result: 42"
-
-### tool_arguments
-
-Extracts arguments from a specific tool call.
-
-```yaml
-graders:
- search_query:
- kind: tool
- function: contains
- extractor: tool_arguments # Extract tool call arguments
- extractor_config:
- tool_name: search # Which tool to extract from
-```
-
-Returns the JSON arguments as a string.
-
-Example: If agent calls `search(query="pandas", limit=10)`, extracts:
-```json
-{"query": "pandas", "limit": 10}
-```
-
-### tool_output
-
-Extracts the return value from a specific tool call.
-
-```yaml
-graders:
- search_results:
- kind: tool
- function: contains
- extractor: tool_output # Extract tool return value
- extractor_config:
- tool_name: search # Which tool's output to extract
-```
-
-Finds the tool call and its corresponding return message.
-
-### after_marker
-
-Extracts content after a specific marker string.
-
-```yaml
-graders:
- answer_section:
- kind: tool
- function: contains
- extractor: after_marker # Extract content after marker
- extractor_config:
- marker: "ANSWER:" # Marker string to find
- include_marker: false # Don't include "ANSWER:" in output
-```
-
-Example: From "Here's my analysis... ANSWER: Paris", extracts "Paris"
-
-### memory_block
-
-Extracts content from a specific memory block (requires agent_state).
-
-```yaml
-graders:
- human_memory:
- kind: tool
- function: exact_match
- extractor: memory_block # Extract from agent memory
- extractor_config:
- block_label: human # Which memory block to extract
-```
-
-**Important**: This extractor requires the agent's final state, which adds overhead. The runner automatically fetches agent_state when this extractor is used.
-
-Example use case: Verify the agent correctly updated its memory about the user.
-
-## Extractor Configuration
-
-Some extractors accept additional configuration via `extractor_config`:
-
-```yaml
-graders:
- my_metric:
- kind: tool
- function: contains
- extractor: pattern # Use pattern extractor
- extractor_config: # Configuration for this extractor
- pattern: 'Answer: (.*)' # Regex pattern
- group: 1 # Extract capture group 1
-```
-
-## Choosing an Extractor
-
-| Use Case | Recommended Extractor |
-|----------|---------------------|
-| Final agent response | `last_assistant` |
-| First response before tools | `first_assistant` |
-| Complete conversation | `all_assistant` |
-| Specific format extraction | `pattern` |
-| Tool usage validation | `tool_arguments` |
-| Tool result checking | `tool_output` |
-| Memory validation | `memory_block` |
-| Structured output | `after_marker` |
-
-## Content Flattening
-
-Assistant messages can contain multiple content parts. Extractors automatically flatten complex content to plain text.
-
-## Empty Extraction
-
-If an extractor finds no matching content, it returns an empty string `""`. This typically results in a score of 0.0 from the grader.
-
-## Custom Extractors
-
-You can write custom extractors. See [Custom Extractors](../extractors/custom.md) for details.
-
-Example:
-
-```python
-from letta_evals.decorators import extractor
-from letta_client import LettaMessageUnion
-
-@extractor
-def my_extractor(trajectory: List[List[LettaMessageUnion]], config: dict) -> str:
- # Custom extraction logic
- return extracted_text
-```
-
-Register by importing in your suite's setup script or custom evaluators file.
-
-## Multi-Metric Extraction
-
-Different graders can use different extractors:
-
-```yaml
-graders:
- response_quality: # Evaluate final message quality
- kind: rubric
- prompt_path: quality.txt
- extractor: last_assistant # Extract final response
-
- tool_usage: # Check tool was called correctly
- kind: tool
- function: exact_match
- extractor: tool_arguments # Extract tool args
- extractor_config:
- tool_name: search # From search tool
-
- memory_update: # Verify memory updated
- kind: tool
- function: contains
- extractor: memory_block # Extract from memory
- extractor_config:
- block_label: human # Human memory block
-```
-
-Each grader independently extracts and evaluates different aspects.
-
-## Listing Extractors
-
-See all available extractors:
-
-```bash
-letta-evals list-extractors
-```
-
-## Examples
-
-### Extract Final Answer
-
-```yaml
-extractor: last_assistant # Get final agent message
-```
-
-Agent: "Let me search... *uses tool* ... The answer is Paris."
-Extracted: "The answer is Paris."
-
-### Extract Tool Arguments
-
-```yaml
-extractor: tool_arguments # Get tool call args
-extractor_config:
- tool_name: search # From search tool
-```
-
-Agent calls: `search(query="pandas", limit=5)`
-Extracted: `{"query": "pandas", "limit": 5}`
-
-### Extract Pattern
-
-```yaml
-extractor: pattern # Extract with regex
-extractor_config:
- pattern: 'RESULT: (\w+)' # Match pattern
- group: 1 # Extract capture group 1
-```
-
-Agent: "After calculation... RESULT: SUCCESS"
-Extracted: "SUCCESS"
-
-### Extract Memory
-
-```yaml
-extractor: memory_block # Extract from agent memory
-extractor_config:
- block_label: human # Human memory block
-```
-
-Agent updates memory block "human" to: "User's name is Alice"
-Extracted: "User's name is Alice"
-
-## Troubleshooting
-
-### Extractor returns empty string
-
-**Problem**: Grader always gives score 0.0 because extractor finds nothing.
-
-**Common causes**:
-- **Wrong extractor**: Using `first_assistant` but agent doesn't respond until after tool use → use `last_assistant`
-- **Wrong tool name**: `tool_arguments` with `tool_name: "search"` but agent calls `"web_search"` → check actual tool name
-- **Wrong memory block**: `memory_block` with `block_label: "user"` but block is actually labeled `"human"` → check block labels
-- **Pattern doesn't match**: `pattern: "Answer: (.*)"` but agent says "The answer is..." → adjust regex
-
-**Debug tips**:
-1. Check the trajectory in results JSON to see actual agent output
-2. Use `last_assistant` first to see what's there
-3. Verify tool names with `letta-evals list-extractors`
-
-### Pattern extractor not working
-
-**Problem**: Pattern extractor returns empty or wrong content.
-
-**Solutions**:
-- Test your regex separately first
-- Remember to escape special characters: `\.`, `\(`, `\)`
-- Use `group: 0` to see the full match (default)
-- Use `group: 1` to extract first capture group
-- Set `search_all: true` if you need all matches
-
-### Memory block extractor fails
-
-**Problem**: `memory_block` extractor causes errors or returns nothing.
-
-**Solutions**:
-- Verify the block label exactly matches (case-sensitive)
-- Check that agent actually has this memory block
-- Remember: this adds overhead by fetching agent state
-
-### Tool extractor finds wrong tool
-
-**Problem**: Multiple tool calls, but extractor gets the wrong one.
-
-**Current behavior**: Extractors get the **first** matching tool call.
-
-**Workaround**: Use custom extractor to implement more specific logic.
-
-## Next Steps
-
-- [Built-in Extractors Reference](../extractors/builtin.md) - Complete extractor documentation
-- [Custom Extractors Guide](../extractors/custom.md) - Write your own extractors
-- [Graders](./graders.md) - How to use extractors with graders
diff --git a/fern/pages/evals/concepts/extractors.mdx b/fern/pages/evals/concepts/extractors.mdx
deleted file mode 100644
index 470bf23d..00000000
--- a/fern/pages/evals/concepts/extractors.mdx
+++ /dev/null
@@ -1,374 +0,0 @@
-# Extractors
-
-**Extractors** select what content to evaluate from an agent's response. They navigate the conversation trajectory and extract the specific piece you want to grade.
-
-
-**Quick overview:**
-- **Purpose**: Agent responses are complex (messages, tool calls, memory) - extractors isolate what to grade
-- **Built-in options**: last_assistant, tool_arguments, memory_block, pattern, and more
-- **Flexible**: Different graders can use different extractors in the same suite
-- **Automatic**: No setup needed - just specify in your grader config
-
-
-**Common patterns:**
-- `last_assistant` - Most common, gets the agent's final message (90% of use cases)
-- `tool_arguments` - Verify agent called the right tool with correct args
-- `memory_block` - Check if agent updated memory correctly
-- `pattern` - Extract structured data with regex
-
-Extractors determine what part of the agent's response gets graded. They pull out specific content from the conversation trajectory.
-
-## Why Extractors?
-
-An agent's response is complex - it includes assistant messages, tool calls, tool returns, memory updates, etc. Extractors let you focus on exactly what you want to evaluate.
-
-**The evaluation flow:**
-```
-Agent Response → Extractor → Submission Text → Grader → Score
-```
-
-For example:
-```
-Full trajectory:
- UserMessage: "What's the capital of France?"
- ToolCallMessage: search(query="capital of france")
- ToolReturnMessage: "Paris is the capital..."
- AssistantMessage: "The capital of France is Paris."
-
-↓ extractor: last_assistant ↓
-
-Extracted: "The capital of France is Paris."
-
-↓ grader: contains (ground_truth="Paris") ↓
-
-Score: 1.0
-```
-
-## Trajectory Structure
-
-A trajectory is a list of turns, where each turn is a list of Letta messages:
-
-```python
-[
- [UserMessage(...), AssistantMessage(...), ToolCallMessage(...), ToolReturnMessage(...)], # Turn 1
- [AssistantMessage(...)] # Turn 2
-]
-```
-
-Extractors navigate this structure to pull out the submission text.
-
-## Built-in Extractors
-
-### last_assistant
-
-Extracts the last assistant message content.
-
-```yaml
-graders:
- quality:
- kind: tool
- function: contains
- extractor: last_assistant # Extract final agent message
-```
-
-Most common extractor - gets the agent's final response.
-
-### first_assistant
-
-Extracts the first assistant message content.
-
-```yaml
-graders:
- initial_response:
- kind: tool
- function: contains
- extractor: first_assistant # Extract first agent message
-```
-
-Useful for testing immediate responses before tool usage.
-
-### all_assistant
-
-Concatenates all assistant messages with a separator.
-
-```yaml
-graders:
- complete_response:
- kind: rubric
- prompt_path: rubric.txt
- extractor: all_assistant # Concatenate all agent messages
- extractor_config:
- separator: "\n\n" # Join messages with double newline
-```
-
-Use when you need the full conversation context.
-
-### last_turn
-
-Extracts all assistant messages from the last turn only.
-
-```yaml
-graders:
- final_turn:
- kind: tool
- function: contains
- extractor: last_turn # Messages from final turn only
- extractor_config:
- separator: " " # Join with spaces
-```
-
-Useful when the agent makes multiple statements in the final turn.
-
-### pattern
-
-Extracts content matching a regex pattern from assistant messages.
-
-```yaml
-graders:
- extract_number:
- kind: tool
- function: exact_match
- extractor: pattern # Extract using regex
- extractor_config:
- pattern: 'Result: (\d+)' # Regex pattern to match
- group: 1 # Extract capture group 1
- search_all: false # Only find first match
-```
-
-Example: Extract "42" from "The answer is Result: 42"
-
-### tool_arguments
-
-Extracts arguments from a specific tool call.
-
-```yaml
-graders:
- search_query:
- kind: tool
- function: contains
- extractor: tool_arguments # Extract tool call arguments
- extractor_config:
- tool_name: search # Which tool to extract from
-```
-
-Returns the JSON arguments as a string.
-
-Example: If agent calls `search(query="pandas", limit=10)`, extracts:
-```json
-{"query": "pandas", "limit": 10}
-```
-
-### tool_output
-
-Extracts the return value from a specific tool call.
-
-```yaml
-graders:
- search_results:
- kind: tool
- function: contains
- extractor: tool_output # Extract tool return value
- extractor_config:
- tool_name: search # Which tool's output to extract
-```
-
-Finds the tool call and its corresponding return message.
-
-### after_marker
-
-Extracts content after a specific marker string.
-
-```yaml
-graders:
- answer_section:
- kind: tool
- function: contains
- extractor: after_marker # Extract content after marker
- extractor_config:
- marker: "ANSWER:" # Marker string to find
- include_marker: false # Don't include "ANSWER:" in output
-```
-
-Example: From "Here's my analysis... ANSWER: Paris", extracts "Paris"
-
-### memory_block
-
-Extracts content from a specific memory block (requires agent_state).
-
-```yaml
-graders:
- human_memory:
- kind: tool
- function: exact_match
- extractor: memory_block # Extract from agent memory
- extractor_config:
- block_label: human # Which memory block to extract
-```
-
-
-**Important**: This extractor requires the agent's final state, which adds overhead. The runner automatically fetches agent_state when this extractor is used.
-
-
-Example use case: Verify the agent correctly updated its memory about the user.
-
-## Extractor Configuration
-
-Some extractors accept additional configuration via `extractor_config`:
-
-```yaml
-graders:
- my_metric:
- kind: tool
- function: contains
- extractor: pattern # Use pattern extractor
- extractor_config: # Configuration for this extractor
- pattern: 'Answer: (.*)' # Regex pattern
- group: 1 # Extract capture group 1
-```
-
-## Choosing an Extractor
-
-| Use Case | Recommended Extractor |
-|----------|---------------------|
-| Final agent response | `last_assistant` |
-| First response before tools | `first_assistant` |
-| Complete conversation | `all_assistant` |
-| Specific format extraction | `pattern` |
-| Tool usage validation | `tool_arguments` |
-| Tool result checking | `tool_output` |
-| Memory validation | `memory_block` |
-| Structured output | `after_marker` |
-
-## Content Flattening
-
-Assistant messages can contain multiple content parts. Extractors automatically flatten complex content to plain text.
-
-## Empty Extraction
-
-If an extractor finds no matching content, it returns an empty string `""`. This typically results in a score of 0.0 from the grader.
-
-## Custom Extractors
-
-You can write custom extractors. See [Custom Extractors](/guides/evals/extractors/custom-extractors) for details.
-
-Example:
-
-```python
-from letta_evals.decorators import extractor
-from letta_client import LettaMessageUnion
-
-@extractor
-def my_extractor(trajectory: List[List[LettaMessageUnion]], config: dict) -> str:
- # Custom extraction logic
- return extracted_text
-```
-
-Register by importing in your suite's setup script or custom evaluators file.
-
-## Multi-Metric Extraction
-
-Different graders can use different extractors:
-
-```yaml
-graders:
- response_quality: # Evaluate final message quality
- kind: rubric
- prompt_path: quality.txt
- extractor: last_assistant # Extract final response
-
- tool_usage: # Check tool was called correctly
- kind: tool
- function: exact_match
- extractor: tool_arguments # Extract tool args
- extractor_config:
- tool_name: search # From search tool
-
- memory_update: # Verify memory updated
- kind: tool
- function: contains
- extractor: memory_block # Extract from memory
- extractor_config:
- block_label: human # Human memory block
-```
-
-Each grader independently extracts and evaluates different aspects.
-
-## Listing Extractors
-
-See all available extractors:
-
-```bash
-letta-evals list-extractors
-```
-
-## Examples
-
-### Extract Final Answer
-
-```yaml
-extractor: last_assistant # Get final agent message
-```
-
-Agent: "Let me search... *uses tool* ... The answer is Paris."
-Extracted: "The answer is Paris."
-
-### Extract Tool Arguments
-
-```yaml
-extractor: tool_arguments # Get tool call args
-extractor_config:
- tool_name: search # From search tool
-```
-
-Agent calls: `search(query="pandas", limit=5)`
-Extracted: `{"query": "pandas", "limit": 5}`
-
-### Extract Pattern
-
-```yaml
-extractor: pattern # Extract with regex
-extractor_config:
- pattern: 'RESULT: (\w+)' # Match pattern
- group: 1 # Extract capture group 1
-```
-
-Agent: "After calculation... RESULT: SUCCESS"
-Extracted: "SUCCESS"
-
-### Extract Memory
-
-```yaml
-extractor: memory_block # Extract from agent memory
-extractor_config:
- block_label: human # Human memory block
-```
-
-Agent updates memory block "human" to: "User's name is Alice"
-Extracted: "User's name is Alice"
-
-## Troubleshooting
-
-
-**Extractor returns empty string**
-
-**Problem**: Grader always gives score 0.0 because extractor finds nothing.
-
-**Common causes**:
-- **Wrong extractor**: Using `first_assistant` but agent doesn't respond until after tool use → use `last_assistant`
-- **Wrong tool name**: `tool_arguments` with `tool_name: "search"` but agent calls `"web_search"` → check actual tool name
-- **Wrong memory block**: `memory_block` with `block_label: "user"` but block is actually labeled `"human"` → check block labels
-- **Pattern doesn't match**: `pattern: "Answer: (.*)"` but agent says "The answer is..." → adjust regex
-
-
-
-**Debug tips**:
-1. Check the trajectory in results JSON to see actual agent output
-2. Use `last_assistant` first to see what's there
-3. Verify tool names with `letta-evals list-extractors`
-
-
-## Next Steps
-
-- [Built-in Extractors Reference](/guides/evals/extractors/built-in-extractors) - Complete extractor documentation
-- [Custom Extractors Guide](/guides/evals/extractors/custom-extractors) - Write your own extractors
-- [Graders](/guides/evals/concepts/graders) - How to use extractors with graders
diff --git a/fern/pages/evals/concepts/gates.md b/fern/pages/evals/concepts/gates.md
deleted file mode 100644
index 94db3f75..00000000
--- a/fern/pages/evals/concepts/gates.md
+++ /dev/null
@@ -1,375 +0,0 @@
-# Gates
-
-**Gates** are the pass/fail criteria for your evaluation. They determine whether your agent meets the required performance threshold by checking aggregate metrics.
-
-**Quick overview:**
-- **Single decision**: One gate per suite determines pass/fail
-- **Two metrics**: `avg_score` (average of all scores) or `accuracy` (percentage passing threshold)
-- **Flexible operators**: >=, >, <=, <, == for threshold comparison
-- **Customizable pass criteria**: Define what counts as "passing" for accuracy calculations
-- **Exit codes**: Suite exits 0 for pass, 1 for fail
-
-**Common patterns:**
-- `avg_score >= 0.8` - Average score must be 80%+
-- `accuracy >= 0.9` - 90%+ of samples must pass
-- Custom threshold - Define per-sample pass criteria with `pass_value`
-
-Gates define the pass/fail criteria for your evaluation. They check if aggregate metrics meet a threshold.
-
-## Basic Structure
-
-```yaml
-gate:
- metric_key: accuracy # Which grader to evaluate
- metric: avg_score # Use average score (default)
- op: gte # Greater than or equal
- value: 0.8 # 80% threshold
-```
-
-## Why Use Gates?
-
-Gates provide **automated pass/fail decisions** for your evaluations, which is essential for:
-
-**CI/CD Integration**: Gates let you block deployments if agent performance drops:
-```bash
-letta-evals run suite.yaml
-# Exit code 0 = pass (continue deployment)
-# Exit code 1 = fail (block deployment)
-```
-
-**Regression Testing**: Set a baseline threshold and ensure new changes don't degrade performance:
-```yaml
-gate:
- metric: avg_score
- op: gte
- value: 0.85 # Must maintain 85%+ to pass
-```
-
-**Quality Enforcement**: Require agents meet minimum standards before production:
-```yaml
-gate:
- metric: accuracy
- op: gte
- value: 0.95 # 95% of test cases must pass
-```
-
-### What Happens When Gates Fail?
-
-When a gate condition is not met:
-
-1. **Console output** shows failure message:
- ```
- ✗ FAILED (0.72/1.00 avg, 72.0% pass rate)
- Gate check failed: avg_score (0.72) not >= 0.80
- ```
-
-2. **Exit code** is 1 (non-zero indicates failure):
- ```bash
- letta-evals run suite.yaml
- echo $? # Prints 1 if gate failed
- ```
-
-3. **Results JSON** includes `gate_passed: false`:
- ```json
- {
- "gate_passed": false,
- "gate_check": {
- "metric": "avg_score",
- "value": 0.72,
- "threshold": 0.80,
- "operator": "gte",
- "passed": false
- },
- "metrics": { ... }
- }
- ```
-
-4. **All other data is preserved** - you still get full results, scores, and trajectories even when gating fails
-
-**Common use case in CI**:
-```bash
-#!/bin/bash
-letta-evals run suite.yaml --output results.json
-
-if [ $? -ne 0 ]; then
- echo "❌ Agent evaluation failed - blocking merge"
- exit 1
-else
- echo "✅ Agent evaluation passed - safe to merge"
-fi
-```
-
-## Required Fields
-
-### metric_key
-
-Which grader to evaluate. Must match a key in your `graders` section:
-
-```yaml
-graders:
- accuracy: # Grader name
- kind: tool
- function: exact_match
- extractor: last_assistant
-
-gate:
- metric_key: accuracy # Must match grader name above
- op: gte # >=
- value: 0.8 # 80% threshold
-```
-
-If you only have one grader, `metric_key` can be omitted - it will default to your single grader.
-
-### metric
-
-Which aggregate statistic to compare. Two options:
-
-#### avg_score
-
-Average score across all samples (0.0 to 1.0):
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- metric: avg_score # Use average of all scores
- op: gte # >=
- value: 0.7 # Must average 70%+
-```
-
-Example: If scores are [0.8, 0.9, 0.6], avg_score = 0.77
-
-#### accuracy
-
-Pass rate as a percentage (0.0 to 1.0):
-
-```yaml
-gate:
- metric_key: accuracy # Check accuracy grader
- metric: accuracy # Use pass rate, not average
- op: gte # >=
- value: 0.8 # 80% of samples must pass
-```
-
-By default, samples with score >= 1.0 are considered "passing".
-
-You can customize the per-sample threshold with `pass_op` and `pass_value` (see below).
-
-**Note**: The default `metric` is `avg_score`, so you can omit it if that's what you want:
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- op: gte # >=
- value: 0.7 # 70% threshold (defaults to avg_score)
-```
-
-### op
-
-Comparison operator:
-
-- `gte`: Greater than or equal (>=)
-- `gt`: Greater than (>)
-- `lte`: Less than or equal (<=)
-- `lt`: Less than (<)
-- `eq`: Equal (==)
-
-Most common: `gte` (at least X)
-
-### value
-
-Threshold value for comparison:
-
-- For `avg_score`: 0.0 to 1.0
-- For `accuracy`: 0.0 to 1.0 (representing percentage)
-
-```yaml
-gate:
- metric: avg_score # Average score
- op: gte # >=
- value: 0.75 # 75% threshold
-```
-
-```yaml
-gate:
- metric: accuracy # Pass rate
- op: gte # >=
- value: 0.9 # 90% must pass
-```
-
-## Optional Fields
-
-### pass_op and pass_value
-
-Customize when individual samples are considered "passing" (used for accuracy calculation):
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- metric: accuracy # Use pass rate
- op: gte # >=
- value: 0.8 # 80% must pass
- pass_op: gte # Sample passes if >=
- pass_value: 0.7 # This threshold (70%)
-```
-
-Default behavior:
-- If `metric` is `avg_score`: samples pass if score >= the gate value
-- If `metric` is `accuracy`: samples pass if score >= 1.0 (perfect)
-
-## Examples
-
-### Require 80% Average Score
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- metric: avg_score # Use average
- op: gte # >=
- value: 0.8 # 80% average
-```
-
-Passes if the average score across all samples is >= 0.8
-
-### Require 90% Pass Rate (Perfect Scores)
-
-```yaml
-gate:
- metric_key: accuracy # Check accuracy grader
- metric: accuracy # Use pass rate
- op: gte # >=
- value: 0.9 # 90% must pass (default: score >= 1.0 to pass)
-```
-
-Passes if 90% of samples have score = 1.0
-
-### Require 75% Pass Rate (Score >= 0.7)
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- metric: accuracy # Use pass rate
- op: gte # >=
- value: 0.75 # 75% must pass
- pass_op: gte # Sample passes if >=
- pass_value: 0.7 # 70% threshold per sample
-```
-
-Passes if 75% of samples have score >= 0.7
-
-### Maximum Error Rate
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- metric: accuracy # Use pass rate
- op: gte # >=
- value: 0.95 # 95% must pass (allows 5% failures)
- pass_op: gt # Sample passes if >
- pass_value: 0.0 # 0.0 (any non-zero score)
-```
-
-Allows up to 5% failures.
-
-### Exact Pass Rate
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- metric: accuracy # Use pass rate
- op: eq # Exactly equal
- value: 1.0 # 100% (all samples must pass)
-```
-
-All samples must pass.
-
-## Multi-Metric Gating
-
-When you have multiple graders, you can only gate on one metric:
-
-```yaml
-graders:
- accuracy: # First metric
- kind: tool
- function: exact_match
- extractor: last_assistant
-
- completeness: # Second metric
- kind: rubric
- prompt_path: completeness.txt
- model: gpt-4o-mini
- extractor: last_assistant
-
-gate:
- metric_key: accuracy # Only gate on accuracy (completeness still computed)
- metric: avg_score # Use average
- op: gte # >=
- value: 0.8 # 80% threshold
-```
-
-The evaluation passes/fails based on the gated metric, but results include scores for all metrics.
-
-## Understanding avg_score vs accuracy
-
-### avg_score
-- Arithmetic mean of all scores
-- Sensitive to partial credit
-- Good for continuous evaluation
-
-Example:
-- Scores: [1.0, 0.8, 0.6]
-- avg_score = (1.0 + 0.8 + 0.6) / 3 = 0.8
-
-### accuracy
-- Percentage of samples meeting a threshold
-- Binary pass/fail per sample
-- Good for strict requirements
-
-Example:
-- Scores: [1.0, 0.8, 0.6]
-- pass_value: 0.7
-- Passing: [1.0, 0.8] = 2 out of 3
-- accuracy = 2/3 = 0.667 (66.7%)
-
-## Errors and Attempted Samples
-
-If a sample fails (error during evaluation), it:
-- Gets a score of 0.0
-- Counts toward `total` but not `total_attempted`
-- Included in `avg_score_total` but not `avg_score_attempted`
-
-You can gate on either:
-- `avg_score_total`: Includes errors as 0.0
-- `avg_score_attempted`: Excludes errors (only successfully attempted samples)
-
-**Note**: The `metric` field currently only supports `avg_score` and `accuracy`. By default, gates use `avg_score_attempted`.
-
-## Gate Results
-
-After evaluation, you'll see:
-
-```
-✓ PASSED (2.25/3.00 avg, 75.0% pass rate)
-```
-
-or
-
-```
-✗ FAILED (1.80/3.00 avg, 60.0% pass rate)
-```
-
-The evaluation exit code reflects the gate result:
-- 0: Passed
-- 1: Failed
-
-## Advanced Gating
-
-For complex gating logic (e.g., "pass if accuracy >= 80% OR avg_score >= 0.9"), you'll need to:
-1. Run evaluation with one gate
-2. Examine the results JSON
-3. Apply custom logic in a post-processing script
-
-## Next Steps
-
-- [Understanding Results](../results/overview.md) - Interpreting evaluation output
-- [Multi-Metric Evaluation](../graders/multi-metric.md) - Using multiple graders
-- [Suite YAML Reference](../configuration/suite-yaml.md) - Complete gate configuration
diff --git a/fern/pages/evals/concepts/gates.mdx b/fern/pages/evals/concepts/gates.mdx
deleted file mode 100644
index e6552028..00000000
--- a/fern/pages/evals/concepts/gates.mdx
+++ /dev/null
@@ -1,384 +0,0 @@
-# Gates
-
-**Gates** are the pass/fail criteria for your evaluation. They determine whether your agent meets the required performance threshold by checking aggregate metrics.
-
-
-**Quick overview:**
-- **Single decision**: One gate per suite determines pass/fail
-- **Two metrics**: `avg_score` (average of all scores) or `accuracy` (percentage passing threshold)
-- **Flexible operators**: `>=`, `>`, `<=`, `<`, `==` for threshold comparison
-- **Customizable pass criteria**: Define what counts as "passing" for accuracy calculations
-- **Exit codes**: Suite exits 0 for pass, 1 for fail
-
-
-**Common patterns:**
-- Average score must be 80%+: `avg_score >= 0.8`
-- 90%+ of samples must pass: `accuracy >= 0.9`
-- Custom threshold: Define per-sample pass criteria with `pass_value`
-
-Gates define the pass/fail criteria for your evaluation. They check if aggregate metrics meet a threshold.
-
-## Basic Structure
-
-```yaml
-gate:
- metric_key: accuracy # Which grader to evaluate
- metric: avg_score # Use average score (default)
- op: gte # Greater than or equal
- value: 0.8 # 80% threshold
-```
-
-## Why Use Gates?
-
-Gates provide **automated pass/fail decisions** for your evaluations, which is essential for:
-
-**CI/CD Integration**: Gates let you block deployments if agent performance drops:
-```bash
-letta-evals run suite.yaml
-# Exit code 0 = pass (continue deployment)
-# Exit code 1 = fail (block deployment)
-```
-
-**Regression Testing**: Set a baseline threshold and ensure new changes don't degrade performance:
-```yaml
-gate:
- metric: avg_score
- op: gte
- value: 0.85 # Must maintain 85%+ to pass
-```
-
-**Quality Enforcement**: Require agents meet minimum standards before production:
-```yaml
-gate:
- metric: accuracy
- op: gte
- value: 0.95 # 95% of test cases must pass
-```
-
-### What Happens When Gates Fail?
-
-When a gate condition is not met:
-
-1. **Console output** shows failure message:
- ```text
- ✗ FAILED (0.72/1.00 avg, 72.0% pass rate)
- Gate check failed: avg_score (0.72) not >= 0.80
- ```
-
-2. **Exit code** is 1 (non-zero indicates failure):
- ```bash
- letta-evals run suite.yaml
- echo $? # Prints 1 if gate failed
- ```
-
-3. **Results JSON** includes `gate_passed: false`:
- ```json
- {
- "gate_passed": false,
- "gate_check": {
- "metric": "avg_score",
- "value": 0.72,
- "threshold": 0.80,
- "operator": "gte",
- "passed": false
- },
- "metrics": { ... }
- }
- ```
-
-4. **All other data is preserved** - you still get full results, scores, and trajectories even when gating fails
-
-
-**Common use case in CI**:
-
-```bash
-#!/bin/bash
-letta-evals run suite.yaml --output results.json
-
-if [ $? -ne 0 ]; then
- echo "❌ Agent evaluation failed - blocking merge"
- exit 1
-else
- echo "✅ Agent evaluation passed - safe to merge"
-fi
-```
-
-
-## Required Fields
-
-### metric_key
-
-Which grader to evaluate. Must match a key in your `graders` section:
-
-```yaml
-graders:
- accuracy: # Grader name
- kind: tool
- function: exact_match
- extractor: last_assistant
-
-gate:
- metric_key: accuracy # Must match grader name above
- op: gte # >=
- value: 0.8 # 80% threshold
-```
-
-If you only have one grader, `metric_key` can be omitted - it will default to your single grader.
-
-### metric
-
-Which aggregate statistic to compare. Two options:
-
-#### avg_score
-
-Average score across all samples (0.0 to 1.0):
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- metric: avg_score # Use average of all scores
- op: gte # >=
- value: 0.7 # Must average 70%+
-```
-
-Example: If scores are [0.8, 0.9, 0.6], avg_score = 0.77
-
-#### accuracy
-
-Pass rate as a percentage (0.0 to 1.0):
-
-```yaml
-gate:
- metric_key: accuracy # Check accuracy grader
- metric: accuracy # Use pass rate, not average
- op: gte # >=
- value: 0.8 # 80% of samples must pass
-```
-
-By default, samples with score `>= 1.0` are considered "passing".
-
-You can customize the per-sample threshold with `pass_op` and `pass_value` (see below).
-
-
-**Note**: The default `metric` is `avg_score`, so you can omit it if that's what you want:
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- op: gte # >=
- value: 0.7 # 70% threshold (defaults to avg_score)
-```
-
-
-### op
-
-Comparison operator:
-
-- `gte`: Greater than or equal (`>=`)
-- `gt`: Greater than (`>`)
-- `lte`: Less than or equal (`<=`)
-- `lt`: Less than (`<`)
-- `eq`: Equal (`==`)
-
-Most common: `gte` (at least X)
-
-### value
-
-Threshold value for comparison:
-
-- For `avg_score`: 0.0 to 1.0
-- For `accuracy`: 0.0 to 1.0 (representing percentage)
-
-```yaml
-gate:
- metric: avg_score # Average score
- op: gte # >=
- value: 0.75 # 75% threshold
-```
-
-```yaml
-gate:
- metric: accuracy # Pass rate
- op: gte # >=
- value: 0.9 # 90% must pass
-```
-
-## Optional Fields
-
-### pass_op and pass_value
-
-Customize when individual samples are considered "passing" (used for accuracy calculation):
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- metric: accuracy # Use pass rate
- op: gte # >=
- value: 0.8 # 80% must pass
- pass_op: gte # Sample passes if >=
- pass_value: 0.7 # This threshold (70%)
-```
-
-Default behavior:
-- If `metric` is `avg_score`: samples pass if score `>=` the gate value
-- If `metric` is `accuracy`: samples pass if score `>= 1.0` (perfect)
-
-## Examples
-
-### Require 80% Average Score
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- metric: avg_score # Use average
- op: gte # >=
- value: 0.8 # 80% average
-```
-
-Passes if the average score across all samples is `>= 0.8`
-
-### Require 90% Pass Rate (Perfect Scores)
-
-```yaml
-gate:
- metric_key: accuracy # Check accuracy grader
- metric: accuracy # Use pass rate
- op: gte # >=
- value: 0.9 # 90% must pass (default: score >= 1.0 to pass)
-```
-
-Passes if 90% of samples have score = 1.0
-
-### Require 75% Pass Rate (Score `>= 0.7`)
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- metric: accuracy # Use pass rate
- op: gte # >=
- value: 0.75 # 75% must pass
- pass_op: gte # Sample passes if >=
- pass_value: 0.7 # 70% threshold per sample
-```
-
-Passes if 75% of samples have score `>= 0.7`
-
-### Maximum Error Rate
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- metric: accuracy # Use pass rate
- op: gte # >=
- value: 0.95 # 95% must pass (allows 5% failures)
- pass_op: gt # Sample passes if >
- pass_value: 0.0 # 0.0 (any non-zero score)
-```
-
-Allows up to 5% failures.
-
-### Exact Pass Rate
-
-```yaml
-gate:
- metric_key: quality # Check quality grader
- metric: accuracy # Use pass rate
- op: eq # Exactly equal
- value: 1.0 # 100% (all samples must pass)
-```
-
-All samples must pass.
-
-## Multi-Metric Gating
-
-When you have multiple graders, you can only gate on one metric:
-
-```yaml
-graders:
- accuracy: # First metric
- kind: tool
- function: exact_match
- extractor: last_assistant
-
- completeness: # Second metric
- kind: rubric
- prompt_path: completeness.txt
- model: gpt-4o-mini
- extractor: last_assistant
-
-gate:
- metric_key: accuracy # Only gate on accuracy (completeness still computed)
- metric: avg_score # Use average
- op: gte # >=
- value: 0.8 # 80% threshold
-```
-
-The evaluation passes/fails based on the gated metric, but results include scores for all metrics.
-
-## Understanding avg_score vs accuracy
-
-### avg_score
-- Arithmetic mean of all scores
-- Sensitive to partial credit
-- Good for continuous evaluation
-
-Example:
-- Scores: [1.0, 0.8, 0.6]
-- avg_score = (1.0 + 0.8 + 0.6) / 3 = 0.8
-
-### accuracy
-- Percentage of samples meeting a threshold
-- Binary pass/fail per sample
-- Good for strict requirements
-
-Example:
-- Scores: [1.0, 0.8, 0.6]
-- pass_value: 0.7
-- Passing: [1.0, 0.8] = 2 out of 3
-- accuracy = 2/3 = 0.667 (66.7%)
-
-## Errors and Attempted Samples
-
-If a sample fails (error during evaluation), it:
-- Gets a score of 0.0
-- Counts toward `total` but not `total_attempted`
-- Included in `avg_score_total` but not `avg_score_attempted`
-
-You can gate on either:
-- `avg_score_total`: Includes errors as 0.0
-- `avg_score_attempted`: Excludes errors (only successfully attempted samples)
-
-
-**Note**: The `metric` field currently only supports `avg_score` and `accuracy`. By default, gates use `avg_score_attempted`.
-
-
-## Gate Results
-
-After evaluation, you'll see:
-
-```text
-✓ PASSED (2.25/3.00 avg, 75.0% pass rate)
-```
-
-or
-
-```text
-✗ FAILED (1.80/3.00 avg, 60.0% pass rate)
-```
-
-The evaluation exit code reflects the gate result:
-- 0: Passed
-- 1: Failed
-
-## Advanced Gating
-
-For complex gating logic (e.g., "pass if accuracy `>= 80%` OR avg_score `>= 0.9`"), you'll need to:
-1. Run evaluation with one gate
-2. Examine the results JSON
-3. Apply custom logic in a post-processing script
-
-## Next Steps
-
-- [Understanding Results](/evals/results-metrics/understanding-results) - Interpreting evaluation output
-- [Multi-Metric Evaluation](/guides/guides/evals/graders/multi-metric) - Using multiple graders
-- [Suite YAML Reference](/guides/evals/configuration/suite-yaml) - Complete gate configuration
diff --git a/fern/pages/evals/concepts/graders.md b/fern/pages/evals/concepts/graders.md
deleted file mode 100644
index cb970626..00000000
--- a/fern/pages/evals/concepts/graders.md
+++ /dev/null
@@ -1,328 +0,0 @@
-# Graders
-
-**Graders** are the scoring functions that evaluate agent responses. They take the extracted submission (from an extractor) and assign a score between 0.0 (complete failure) and 1.0 (perfect success).
-
-**Quick overview:**
-- **Two types**: Tool graders (deterministic Python functions) and Rubric graders (LLM-as-judge)
-- **Built-in functions**: exact_match, contains, regex_match, ascii_printable_only
-- **Custom graders**: Write your own grading logic
-- **Multi-metric**: Combine multiple graders in one suite
-- **Flexible extraction**: Each grader can use a different extractor
-
-**When to use each:**
-- **Tool graders**: Fast, deterministic, free - perfect for exact matching, patterns, tool validation
-- **Rubric graders**: Flexible, subjective, costs API calls - ideal for quality, creativity, nuanced evaluation
-
-Graders evaluate agent responses and assign scores between 0.0 (complete failure) and 1.0 (perfect success).
-
-## Grader Types
-
-There are two types of graders:
-
-### Tool Graders
-
-Python functions that programmatically compare the submission to ground truth or apply deterministic checks.
-
-```yaml
-graders:
- accuracy:
- kind: tool # Deterministic grading
- function: exact_match # Built-in grading function
- extractor: last_assistant # Use final agent response
-```
-
-Best for:
-- Exact matching
-- Pattern checking
-- Tool call validation
-- Deterministic criteria
-
-### Rubric Graders
-
-LLM-as-judge evaluation using custom prompts and criteria. Can use either direct LLM API calls or a Letta agent as the judge.
-
-**Standard rubric grading (LLM API):**
-```yaml
-graders:
- quality:
- kind: rubric # LLM-as-judge
- prompt_path: rubric.txt # Custom evaluation criteria
- model: gpt-4o-mini # Judge model
- extractor: last_assistant # What to evaluate
-```
-
-**Agent-as-judge (Letta agent):**
-```yaml
-graders:
- agent_judge:
- kind: rubric # Still "rubric" kind
- agent_file: judge.af # Judge agent with submit_grade tool
- prompt_path: rubric.txt # Evaluation criteria
- extractor: last_assistant # What to evaluate
-```
-
-Best for:
-- Subjective quality assessment
-- Open-ended responses
-- Nuanced evaluation
-- Complex criteria
-- Judges that need tools (when using agent-as-judge)
-
-## Built-in Tool Graders
-
-### exact_match
-
-Checks if submission exactly matches ground truth (case-sensitive, whitespace-trimmed).
-
-```yaml
-graders:
- accuracy:
- kind: tool
- function: exact_match # Case-sensitive, whitespace-trimmed
- extractor: last_assistant # Extract final response
-```
-
-Requires: `ground_truth` in dataset
-
-Score: 1.0 if exact match, 0.0 otherwise
-
-### contains
-
-Checks if submission contains ground truth (case-insensitive).
-
-```yaml
-graders:
- contains_answer:
- kind: tool
- function: contains # Case-insensitive substring match
- extractor: last_assistant # Search in final response
-```
-
-Requires: `ground_truth` in dataset
-
-Score: 1.0 if found, 0.0 otherwise
-
-### regex_match
-
-Checks if submission matches a regex pattern in ground truth.
-
-```yaml
-graders:
- pattern:
- kind: tool
- function: regex_match # Pattern matching
- extractor: last_assistant # Check final response
-```
-
-Dataset sample:
-```json
-{"input": "Generate a UUID", "ground_truth": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"}
-```
-
-Score: 1.0 if pattern matches, 0.0 otherwise
-
-### ascii_printable_only
-
-Validates that all characters are printable ASCII (useful for ASCII art, formatted output).
-
-```yaml
-graders:
- ascii_check:
- kind: tool
- function: ascii_printable_only # Validate ASCII characters
- extractor: last_assistant # Check final response
-```
-
-Does not require ground truth.
-
-Score: 1.0 if all characters are printable ASCII, 0.0 if any non-printable characters found
-
-## Rubric Graders
-
-Rubric graders use an LLM to evaluate responses based on custom criteria.
-
-### Basic Configuration
-
-```yaml
-graders:
- quality:
- kind: rubric # LLM-as-judge
- prompt_path: quality_rubric.txt # Evaluation criteria
- model: gpt-4o-mini # Judge model
- temperature: 0.0 # Deterministic
- extractor: last_assistant # What to evaluate
-```
-
-### Rubric Prompt Format
-
-Your rubric file should describe the evaluation criteria. Use placeholders:
-
-- `{input}`: The original input from the dataset
-- `{submission}`: The extracted agent response
-- `{ground_truth}`: Ground truth from dataset (if available)
-
-Example `quality_rubric.txt`:
-```
-Evaluate the response for:
-1. Accuracy: Does it correctly answer the question?
-2. Completeness: Is the answer thorough?
-3. Clarity: Is it well-explained?
-
-Input: {input}
-Expected: {ground_truth}
-Response: {submission}
-
-Score from 0.0 to 1.0 where:
-- 1.0: Perfect response
-- 0.75: Good with minor issues
-- 0.5: Acceptable but incomplete
-- 0.25: Poor quality
-- 0.0: Completely wrong
-```
-
-### Inline Prompt
-
-Instead of a file, you can include the prompt inline:
-
-```yaml
-graders:
- quality:
- kind: rubric # LLM-as-judge
- prompt: | # Inline prompt instead of file
- Evaluate the creativity and originality of the response.
- Score 1.0 for highly creative, 0.0 for generic or unoriginal.
- model: gpt-4o-mini # Judge model
- extractor: last_assistant # What to evaluate
-```
-
-### Model Configuration
-
-```yaml
-graders:
- quality:
- kind: rubric
- prompt_path: rubric.txt # Evaluation criteria
- model: gpt-4o-mini # Judge model
- temperature: 0.0 # Deterministic (0.0-2.0)
- provider: openai # LLM provider (default: openai)
- max_retries: 5 # API retry attempts
- timeout: 120.0 # Request timeout in seconds
-```
-
-Supported providers:
-- `openai` (default)
-
-Models:
-- Any OpenAI-compatible model
-- Special handling for reasoning models (o1, o3) - temperature automatically adjusted to 1.0
-
-### Structured Output
-
-Rubric graders use JSON mode to get structured responses:
-
-```json
-{
- "score": 0.85,
- "rationale": "The response is accurate and complete but could be more concise."
-}
-```
-
-The score is validated to be between 0.0 and 1.0.
-
-## Multi-Metric Configuration
-
-Evaluate multiple aspects in one suite:
-
-```yaml
-graders:
- accuracy: # Tool grader for factual correctness
- kind: tool
- function: contains
- extractor: last_assistant
-
- completeness: # Rubric grader for thoroughness
- kind: rubric
- prompt_path: completeness_rubric.txt
- model: gpt-4o-mini
- extractor: last_assistant
-
- tool_usage: # Tool grader for tool call validation
- kind: tool
- function: exact_match
- extractor: tool_arguments # Extract tool call args
- extractor_config:
- tool_name: search # Which tool to check
-```
-
-Each grader can use a different extractor.
-
-## Extractor Configuration
-
-Every grader must specify an `extractor` to select what to grade:
-
-```yaml
-graders:
- my_metric:
- kind: tool
- function: contains # Grading function
- extractor: last_assistant # What to extract and grade
-```
-
-Some extractors need additional configuration:
-
-```yaml
-graders:
- tool_check:
- kind: tool
- function: contains # Check if ground truth in tool args
- extractor: tool_arguments # Extract tool call arguments
- extractor_config: # Configuration for this extractor
- tool_name: search # Which tool to extract from
-```
-
-See [Extractors](./extractors.md) for all available extractors.
-
-## Custom Graders
-
-You can write custom grading functions. See [Custom Graders](../advanced/custom-graders.md) for details.
-
-## Grader Selection Guide
-
-| Use Case | Recommended Grader |
-|----------|-------------------|
-| Exact answer matching | `exact_match` |
-| Keyword checking | `contains` |
-| Pattern validation | `regex_match` |
-| Tool call validation | `exact_match` with `tool_arguments` extractor |
-| Quality assessment | Rubric grader |
-| Creativity evaluation | Rubric grader |
-| Format checking | Custom tool grader |
-| Multi-criteria evaluation | Multiple graders |
-
-## Score Interpretation
-
-All scores are between 0.0 and 1.0:
-
-- **1.0**: Perfect - meets all criteria
-- **0.75-0.99**: Good - minor issues
-- **0.5-0.74**: Acceptable - notable gaps
-- **0.25-0.49**: Poor - major problems
-- **0.0-0.24**: Failed - did not meet criteria
-
-Tool graders typically return binary scores (0.0 or 1.0), while rubric graders can return any value in the range.
-
-## Error Handling
-
-If grading fails (e.g., network error, invalid format):
-- Score is set to 0.0
-- Rationale includes error message
-- Metadata includes error details
-
-This ensures evaluations can continue even with individual failures.
-
-## Next Steps
-
-- [Tool Graders](../graders/tool-graders.md) - Built-in and custom functions
-- [Rubric Graders](../graders/rubric-graders.md) - LLM-as-judge details
-- [Multi-Metric Evaluation](../graders/multi-metric.md) - Using multiple graders
-- [Extractors](./extractors.md) - Selecting what to grade
diff --git a/fern/pages/evals/concepts/graders.mdx b/fern/pages/evals/concepts/graders.mdx
deleted file mode 100644
index 2df45a1a..00000000
--- a/fern/pages/evals/concepts/graders.mdx
+++ /dev/null
@@ -1,330 +0,0 @@
-# Graders
-
-**Graders** are the scoring functions that evaluate agent responses. They take the extracted submission (from an extractor) and assign a score between 0.0 (complete failure) and 1.0 (perfect success).
-
-
-**Quick overview:**
-- **Two types**: Tool graders (deterministic Python functions) and Rubric graders (LLM-as-judge)
-- **Built-in functions**: exact_match, contains, regex_match, ascii_printable_only
-- **Custom graders**: Write your own grading logic
-- **Multi-metric**: Combine multiple graders in one suite
-- **Flexible extraction**: Each grader can use a different extractor
-
-
-**When to use each:**
-- **Tool graders**: Fast, deterministic, free - perfect for exact matching, patterns, tool validation
-- **Rubric graders**: Flexible, subjective, costs API calls - ideal for quality, creativity, nuanced evaluation
-
-Graders evaluate agent responses and assign scores between 0.0 (complete failure) and 1.0 (perfect success).
-
-## Grader Types
-
-There are two types of graders:
-
-### Tool Graders
-
-Python functions that programmatically compare the submission to ground truth or apply deterministic checks.
-
-```yaml
-graders:
- accuracy:
- kind: tool # Deterministic grading
- function: exact_match # Built-in grading function
- extractor: last_assistant # Use final agent response
-```
-
-Best for:
-- Exact matching
-- Pattern checking
-- Tool call validation
-- Deterministic criteria
-
-### Rubric Graders
-
-LLM-as-judge evaluation using custom prompts and criteria. Can use either direct LLM API calls or a Letta agent as the judge.
-
-**Standard rubric grading (LLM API):**
-```yaml
-graders:
- quality:
- kind: rubric # LLM-as-judge
- prompt_path: rubric.txt # Custom evaluation criteria
- model: gpt-4o-mini # Judge model
- extractor: last_assistant # What to evaluate
-```
-
-**Agent-as-judge (Letta agent):**
-```yaml
-graders:
- agent_judge:
- kind: rubric # Still "rubric" kind
- agent_file: judge.af # Judge agent with submit_grade tool
- prompt_path: rubric.txt # Evaluation criteria
- extractor: last_assistant # What to evaluate
-```
-
-Best for:
-- Subjective quality assessment
-- Open-ended responses
-- Nuanced evaluation
-- Complex criteria
-- Judges that need tools (when using agent-as-judge)
-
-## Built-in Tool Graders
-
-### exact_match
-
-Checks if submission exactly matches ground truth (case-sensitive, whitespace-trimmed).
-
-```yaml
-graders:
- accuracy:
- kind: tool
- function: exact_match # Case-sensitive, whitespace-trimmed
- extractor: last_assistant # Extract final response
-```
-
-Requires: `ground_truth` in dataset
-
-Score: 1.0 if exact match, 0.0 otherwise
-
-### contains
-
-Checks if submission contains ground truth (case-insensitive).
-
-```yaml
-graders:
- contains_answer:
- kind: tool
- function: contains # Case-insensitive substring match
- extractor: last_assistant # Search in final response
-```
-
-Requires: `ground_truth` in dataset
-
-Score: 1.0 if found, 0.0 otherwise
-
-### regex_match
-
-Checks if submission matches a regex pattern in ground truth.
-
-```yaml
-graders:
- pattern:
- kind: tool
- function: regex_match # Pattern matching
- extractor: last_assistant # Check final response
-```
-
-Dataset sample:
-```json
-{"input": "Generate a UUID", "ground_truth": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"}
-```
-
-Score: 1.0 if pattern matches, 0.0 otherwise
-
-### ascii_printable_only
-
-Validates that all characters are printable ASCII (useful for ASCII art, formatted output).
-
-```yaml
-graders:
- ascii_check:
- kind: tool
- function: ascii_printable_only # Validate ASCII characters
- extractor: last_assistant # Check final response
-```
-
-Does not require ground truth.
-
-Score: 1.0 if all characters are printable ASCII, 0.0 if any non-printable characters found
-
-## Rubric Graders
-
-Rubric graders use an LLM to evaluate responses based on custom criteria.
-
-### Basic Configuration
-
-```yaml
-graders:
- quality:
- kind: rubric # LLM-as-judge
- prompt_path: quality_rubric.txt # Evaluation criteria
- model: gpt-4o-mini # Judge model
- temperature: 0.0 # Deterministic
- extractor: last_assistant # What to evaluate
-```
-
-### Rubric Prompt Format
-
-Your rubric file should describe the evaluation criteria. Use placeholders:
-
-- `{input}`: The original input from the dataset
-- `{submission}`: The extracted agent response
-- `{ground_truth}`: Ground truth from dataset (if available)
-
-Example `quality_rubric.txt`:
-```
-Evaluate the response for:
-1. Accuracy: Does it correctly answer the question?
-2. Completeness: Is the answer thorough?
-3. Clarity: Is it well-explained?
-
-Input: {input}
-Expected: {ground_truth}
-Response: {submission}
-
-Score from 0.0 to 1.0 where:
-- 1.0: Perfect response
-- 0.75: Good with minor issues
-- 0.5: Acceptable but incomplete
-- 0.25: Poor quality
-- 0.0: Completely wrong
-```
-
-### Inline Prompt
-
-Instead of a file, you can include the prompt inline:
-
-```yaml
-graders:
- quality:
- kind: rubric # LLM-as-judge
- prompt: | # Inline prompt instead of file
- Evaluate the creativity and originality of the response.
- Score 1.0 for highly creative, 0.0 for generic or unoriginal.
- model: gpt-4o-mini # Judge model
- extractor: last_assistant # What to evaluate
-```
-
-### Model Configuration
-
-```yaml
-graders:
- quality:
- kind: rubric
- prompt_path: rubric.txt # Evaluation criteria
- model: gpt-4o-mini # Judge model
- temperature: 0.0 # Deterministic (0.0-2.0)
- provider: openai # LLM provider (default: openai)
- max_retries: 5 # API retry attempts
- timeout: 120.0 # Request timeout in seconds
-```
-
-Supported providers:
-- `openai` (default)
-
-Models:
-- Any OpenAI-compatible model
-- Special handling for reasoning models (o1, o3) - temperature automatically adjusted to 1.0
-
-### Structured Output
-
-Rubric graders use JSON mode to get structured responses:
-
-```json
-{
- "score": 0.85,
- "rationale": "The response is accurate and complete but could be more concise."
-}
-```
-
-The score is validated to be between 0.0 and 1.0.
-
-## Multi-Metric Configuration
-
-Evaluate multiple aspects in one suite:
-
-```yaml
-graders:
- accuracy: # Tool grader for factual correctness
- kind: tool
- function: contains
- extractor: last_assistant
-
- completeness: # Rubric grader for thoroughness
- kind: rubric
- prompt_path: completeness_rubric.txt
- model: gpt-4o-mini
- extractor: last_assistant
-
- tool_usage: # Tool grader for tool call validation
- kind: tool
- function: exact_match
- extractor: tool_arguments # Extract tool call args
- extractor_config:
- tool_name: search # Which tool to check
-```
-
-Each grader can use a different extractor.
-
-## Extractor Configuration
-
-Every grader must specify an `extractor` to select what to grade:
-
-```yaml
-graders:
- my_metric:
- kind: tool
- function: contains # Grading function
- extractor: last_assistant # What to extract and grade
-```
-
-Some extractors need additional configuration:
-
-```yaml
-graders:
- tool_check:
- kind: tool
- function: contains # Check if ground truth in tool args
- extractor: tool_arguments # Extract tool call arguments
- extractor_config: # Configuration for this extractor
- tool_name: search # Which tool to extract from
-```
-
-See [Extractors](/guides/evals/concepts/extractors) for all available extractors.
-
-## Custom Graders
-
-You can write custom grading functions. See [Custom Graders](/guides/evals/advanced/custom-graders) for details.
-
-## Grader Selection Guide
-
-| Use Case | Recommended Grader |
-|----------|-------------------|
-| Exact answer matching | `exact_match` |
-| Keyword checking | `contains` |
-| Pattern validation | `regex_match` |
-| Tool call validation | `exact_match` with `tool_arguments` extractor |
-| Quality assessment | Rubric grader |
-| Creativity evaluation | Rubric grader |
-| Format checking | Custom tool grader |
-| Multi-criteria evaluation | Multiple graders |
-
-## Score Interpretation
-
-All scores are between 0.0 and 1.0:
-
-- **1.0**: Perfect - meets all criteria
-- **0.75-0.99**: Good - minor issues
-- **0.5-0.74**: Acceptable - notable gaps
-- **0.25-0.49**: Poor - major problems
-- **0.0-0.24**: Failed - did not meet criteria
-
-Tool graders typically return binary scores (0.0 or 1.0), while rubric graders can return any value in the range.
-
-## Error Handling
-
-If grading fails (e.g., network error, invalid format):
-- Score is set to 0.0
-- Rationale includes error message
-- Metadata includes error details
-
-This ensures evaluations can continue even with individual failures.
-
-## Next Steps
-
-- [Tool Graders](/guides/evals/graders/tool-graders) - Built-in and custom functions
-- [Rubric Graders](/guides/evals/graders/rubric-graders) - LLM-as-judge details
-- [Multi-Metric Evaluation](/guides/guides/evals/graders/multi-metric) - Using multiple graders
-- [Extractors](/guides/evals/concepts/extractors) - Selecting what to grade
diff --git a/fern/pages/evals/concepts/overview.md b/fern/pages/evals/concepts/overview.md
deleted file mode 100644
index 6e24bf36..00000000
--- a/fern/pages/evals/concepts/overview.md
+++ /dev/null
@@ -1,205 +0,0 @@
-# Core Concepts Overview
-
-## What is Letta Evals?
-
-Letta Evals is a framework for systematically testing and measuring the performance of Letta AI agents. It provides a structured way to:
-
-- Define test cases and expected behaviors
-- Run agents against those tests automatically
-- Score agent responses using deterministic rules or LLM judges
-- Track performance over time and across different configurations
-
-Think of it as a testing framework specifically designed for stateful agents.
-
-## The Evaluation Flow
-
-Every evaluation follows this flow:
-
-**Dataset → Target (Agent) → Extractor → Grader → Gate → Result**
-
-1. **Dataset**: Your test cases (questions, scenarios, expected outputs)
-2. **Target**: The agent being evaluated
-3. **Extractor**: Pulls out the relevant information from the agent's response
-4. **Grader**: Scores the extracted information
-5. **Gate**: Pass/fail criteria for the overall evaluation
-6. **Result**: Metrics, scores, and detailed results
-
-### Built for Stateful Agents
-
-Unlike most evaluation frameworks designed for simple input-output models, Letta Evals is purpose-built for **stateful agents** - agents that:
-- Maintain memory across conversations
-- Use tools and external functions
-- Evolve their behavior based on interactions
-- Have persistent context and state
-
-This means you can test:
-- **Memory updates**: Did the agent correctly remember the user's name?
-- **Multi-turn conversations**: Can the agent maintain context across multiple exchanges?
-- **Tool usage**: Does the agent call the right tools with the right arguments?
-- **State evolution**: How does the agent's internal state change over time?
-
-Traditional eval frameworks treat each test as independent. Letta Evals understands that agent state matters.
-
-**Example: Testing Memory Updates**
-```yaml
-graders:
- memory_check:
- kind: tool # Deterministic grading
- function: contains # Check if ground_truth in extracted content
- extractor: memory_block # Extract from agent memory (not just response!)
- extractor_config:
- block_label: human # Which memory block to check
-```
-
-Dataset:
-```jsonl
-{"input": "Please remember that I like bananas.", "ground_truth": "bananas"}
-```
-
-This doesn't just check if the agent responded correctly - it verifies the agent actually stored "bananas" in its memory block. Traditional eval frameworks can't inspect agent state like this.
-
-## Why Evals Matter
-
-AI agents are complex systems that can behave unpredictably. Without systematic evaluation, you can't:
-- **Know if changes improve or break your agent** - Did that prompt tweak help or hurt?
-- **Prevent regressions** - Catch when "fixes" break existing functionality
-- **Compare approaches objectively** - Which model works better for your use case?
-- **Build confidence before deployment** - Ensure quality before shipping to users
-- **Track improvement over time** - Measure progress as you iterate
-
-Manual testing doesn't scale. Evals let you test hundreds of scenarios in minutes.
-
-## What Evals Are Useful For
-
-### 1. Development & Iteration
-- Test prompt changes instantly across your entire test suite
-- Experiment with different models and compare results
-- Validate that new features work as expected
-
-### 2. Quality Assurance
-- Prevent regressions when modifying agent behavior
-- Ensure agents handle edge cases correctly
-- Verify tool usage and memory updates
-
-### 3. Model Selection
-- Compare GPT-4 vs Claude vs other models on your specific use case
-- Test different model configurations (temperature, system prompts, etc.)
-- Find the right cost/performance tradeoff
-
-### 4. Benchmarking
-- Measure agent performance on standard tasks
-- Track improvements over time
-- Share reproducible results with your team
-
-### 5. Production Readiness
-- Validate agents meet quality thresholds before deployment
-- Run continuous evaluation in CI/CD pipelines
-- Monitor production agent quality
-
-## How Letta Evals Works
-
-Letta Evals is built around a few key concepts that work together to create a flexible evaluation framework.
-
-## Key Components
-
-### Suite
-
-An **evaluation suite** is a complete test configuration defined in a YAML file. It ties together:
-- Which dataset to use
-- Which agent to test
-- How to grade responses
-- What criteria determine pass/fail
-
-Think of a suite as a reusable test specification.
-
-### Dataset
-
-A **dataset** is a JSONL file where each line represents one test case. Each sample has:
-- An input (what to ask the agent)
-- Optional ground truth (the expected answer)
-- Optional metadata (tags, custom fields)
-
-### Target
-
-The **target** is what you're evaluating. Currently, this is a Letta agent, specified by:
-- An agent file (.af)
-- An existing agent ID
-- A Python script that creates agents programmatically
-
-### Trajectory
-
-A **trajectory** is the complete conversation history from one test case. It's a list of turns, where each turn contains a list of Letta messages (assistant messages, tool calls, tool returns, etc.).
-
-### Extractor
-
-An **extractor** determines what part of the trajectory to evaluate. For example:
-- The last thing the agent said
-- All tool calls made
-- Content from agent memory
-- Text matching a pattern
-
-### Grader
-
-A **grader** scores how well the agent performed. There are two types:
-- **Tool graders**: Python functions that compare submission to ground truth
-- **Rubric graders**: LLM judges that evaluate based on custom criteria
-
-### Gate
-
-A **gate** is the pass/fail threshold for your evaluation. It compares aggregate metrics (like average score or pass rate) against a target value.
-
-## Multi-Metric Evaluation
-
-You can define multiple graders in one suite to evaluate different aspects:
-
-```yaml
-graders:
- accuracy: # Check if answer is correct
- kind: tool
- function: exact_match
- extractor: last_assistant # Use final response
-
- tool_usage: # Check if agent called the right tool
- kind: tool
- function: contains
- extractor: tool_arguments # Extract tool call args
- extractor_config:
- tool_name: search # From search tool
-```
-
-The gate can check any of these metrics:
-
-```yaml
-gate:
- metric_key: accuracy # Gate on accuracy (tool_usage still computed)
- op: gte # >=
- value: 0.8 # 80% threshold
-```
-
-## Score Normalization
-
-All scores are normalized to the range [0.0, 1.0]:
-- 0.0 = complete failure
-- 1.0 = perfect success
-- Values in between = partial credit
-
-This allows different grader types to be compared and combined.
-
-## Aggregate Metrics
-
-Individual sample scores are aggregated in two ways:
-
-1. **Average Score**: Mean of all scores (0.0 to 1.0)
-2. **Accuracy/Pass Rate**: Percentage of samples passing a threshold
-
-You can gate on either metric type.
-
-## Next Steps
-
-Dive deeper into each concept:
-- [Suites](./suites.md) - Suite configuration in detail
-- [Datasets](./datasets.md) - Creating effective test datasets
-- [Targets](./targets.md) - Agent configuration options
-- [Graders](./graders.md) - Understanding grader types
-- [Extractors](./extractors.md) - Extraction strategies
-- [Gates](./gates.md) - Setting pass/fail criteria
diff --git a/fern/pages/evals/concepts/overview.mdx b/fern/pages/evals/concepts/overview.mdx
deleted file mode 100644
index 8ebf85f2..00000000
--- a/fern/pages/evals/concepts/overview.mdx
+++ /dev/null
@@ -1,207 +0,0 @@
-# Core Concepts
-
-Understanding how Letta Evals works and what makes it different.
-
-
-**Just want to run an eval?** Skip to [Getting Started](/guides/evals/getting-started) for a hands-on quickstart.
-
-
-## Built for Stateful Agents
-
-Letta Evals is a testing framework specifically designed for agents that maintain state. Unlike traditional eval frameworks built for simple input-output models, Letta Evals understands that agents:
-
-- Maintain memory across conversations
-- Use tools and external functions
-- Evolve their behavior based on interactions
-- Have persistent context and state
-
-This means you can test aspects of your agent that other frameworks can't: memory updates, multi-turn conversations, tool usage patterns, and state evolution over time.
-
-## The Evaluation Flow
-
-Every evaluation follows this flow:
-
-**Dataset → Target (Agent) → Extractor → Grader → Gate → Result**
-
-1. **Dataset**: Your test cases (questions, scenarios, expected outputs)
-2. **Target**: The agent being evaluated
-3. **Extractor**: Pulls out the relevant information from the agent's response
-4. **Grader**: Scores the extracted information
-5. **Gate**: Pass/fail criteria for the overall evaluation
-6. **Result**: Metrics, scores, and detailed results
-
-### What You Can Test
-
-With Letta Evals, you can test aspects of agents that traditional frameworks can't:
-
-- **Memory updates**: Did the agent correctly remember the user's name?
-- **Multi-turn conversations**: Can the agent maintain context across multiple exchanges?
-- **Tool usage**: Does the agent call the right tools with the right arguments?
-- **State evolution**: How does the agent's internal state change over time?
-
-
-**Example: Testing Memory Updates**
-
-```yaml
-graders:
- memory_check:
- kind: tool # Deterministic grading
- function: contains # Check if ground_truth in extracted content
- extractor: memory_block # Extract from agent memory (not just response!)
- extractor_config:
- block_label: human # Which memory block to check
-```
-
-Dataset:
-```jsonl
-{"input": "Please remember that I like bananas.", "ground_truth": "bananas"}
-```
-
-This doesn't just check if the agent responded correctly - it verifies the agent actually stored "bananas" in its memory block. Traditional eval frameworks can't inspect agent state like this.
-
-
-## Why Evals Matter
-
-AI agents are complex systems that can behave unpredictably. Without systematic evaluation, you can't:
-- **Know if changes improve or break your agent** - Did that prompt tweak help or hurt?
-- **Prevent regressions** - Catch when "fixes" break existing functionality
-- **Compare approaches objectively** - Which model works better for your use case?
-- **Build confidence before deployment** - Ensure quality before shipping to users
-- **Track improvement over time** - Measure progress as you iterate
-
-Manual testing doesn't scale. Evals let you test hundreds of scenarios in minutes.
-
-## What Evals Are Useful For
-
-### 1. Development & Iteration
-- Test prompt changes instantly across your entire test suite
-- Experiment with different models and compare results
-- Validate that new features work as expected
-
-### 2. Quality Assurance
-- Prevent regressions when modifying agent behavior
-- Ensure agents handle edge cases correctly
-- Verify tool usage and memory updates
-
-### 3. Model Selection
-- Compare GPT-4 vs Claude vs other models on your specific use case
-- Test different model configurations (temperature, system prompts, etc.)
-- Find the right cost/performance tradeoff
-
-### 4. Benchmarking
-- Measure agent performance on standard tasks
-- Track improvements over time
-- Share reproducible results with your team
-
-### 5. Production Readiness
-- Validate agents meet quality thresholds before deployment
-- Run continuous evaluation in CI/CD pipelines
-- Monitor production agent quality
-
-## How Letta Evals Works
-
-Letta Evals is built around a few key concepts that work together to create a flexible evaluation framework.
-
-## Key Components
-
-### Suite
-
-An **evaluation suite** is a complete test configuration defined in a YAML file. It ties together:
-- Which dataset to use
-- Which agent to test
-- How to grade responses
-- What criteria determine pass/fail
-
-Think of a suite as a reusable test specification.
-
-### Dataset
-
-A **dataset** is a JSONL file where each line represents one test case. Each sample has:
-- An input (what to ask the agent)
-- Optional ground truth (the expected answer)
-- Optional metadata (tags, custom fields)
-
-### Target
-
-The **target** is what you're evaluating. Currently, this is a Letta agent, specified by:
-- An agent file (.af)
-- An existing agent ID
-- A Python script that creates agents programmatically
-
-### Trajectory
-
-A **trajectory** is the complete conversation history from one test case. It's a list of turns, where each turn contains a list of Letta messages (assistant messages, tool calls, tool returns, etc.).
-
-### Extractor
-
-An **extractor** determines what part of the trajectory to evaluate. For example:
-- The last thing the agent said
-- All tool calls made
-- Content from agent memory
-- Text matching a pattern
-
-### Grader
-
-A **grader** scores how well the agent performed. There are two types:
-- **Tool graders**: Python functions that compare submission to ground truth
-- **Rubric graders**: LLM judges that evaluate based on custom criteria
-
-### Gate
-
-A **gate** is the pass/fail threshold for your evaluation. It compares aggregate metrics (like average score or pass rate) against a target value.
-
-## Multi-Metric Evaluation
-
-You can define multiple graders in one suite to evaluate different aspects:
-
-```yaml
-graders:
- accuracy: # Check if answer is correct
- kind: tool
- function: exact_match
- extractor: last_assistant # Use final response
-
- tool_usage: # Check if agent called the right tool
- kind: tool
- function: contains
- extractor: tool_arguments # Extract tool call args
- extractor_config:
- tool_name: search # From search tool
-```
-
-The gate can check any of these metrics:
-
-```yaml
-gate:
- metric_key: accuracy # Gate on accuracy (tool_usage still computed)
- op: gte # >=
- value: 0.8 # 80% threshold
-```
-
-## Score Normalization
-
-All scores are normalized to the range [0.0, 1.0]:
-- 0.0 = complete failure
-- 1.0 = perfect success
-- Values in between = partial credit
-
-This allows different grader types to be compared and combined.
-
-## Aggregate Metrics
-
-Individual sample scores are aggregated in two ways:
-
-1. **Average Score**: Mean of all scores (0.0 to 1.0)
-2. **Accuracy/Pass Rate**: Percentage of samples passing a threshold
-
-You can gate on either metric type.
-
-## Next Steps
-
-Dive deeper into each concept:
-- [Suites](/guides/evals/concepts/suites) - Suite configuration in detail
-- [Datasets](/guides/evals/concepts/datasets) - Creating effective test datasets
-- [Targets](/guides/evals/concepts/targets) - Agent configuration options
-- [Graders](/guides/evals/concepts/graders) - Understanding grader types
-- [Extractors](/guides/evals/concepts/extractors) - Extraction strategies
-- [Gates](/guides/evals/concepts/gates) - Setting pass/fail criteria
diff --git a/fern/pages/evals/concepts/suites.md b/fern/pages/evals/concepts/suites.md
deleted file mode 100644
index 9bcf33b3..00000000
--- a/fern/pages/evals/concepts/suites.md
+++ /dev/null
@@ -1,273 +0,0 @@
-# Suites
-
-A **suite** is a YAML configuration file that defines a complete evaluation specification. It's the central piece that ties together your dataset, target agent, grading criteria, and pass/fail thresholds.
-
-**Quick overview:**
-- **Single file defines everything**: Dataset, agent, graders, and success criteria all in one YAML
-- **Reusable and shareable**: Version control your evaluation specs alongside your code
-- **Multi-metric support**: Evaluate multiple aspects (accuracy, quality, tool usage) in one run
-- **Multi-model testing**: Run the same suite across different LLM models
-- **Flexible filtering**: Test subsets using tags or sample limits
-
-**Typical workflow:**
-1. Create a suite YAML defining what and how to test
-2. Run `letta-evals run suite.yaml`
-3. Review results showing scores for each metric
-4. Suite passes or fails based on gate criteria
-
-An evaluation suite is a YAML configuration file that defines a complete test specification.
-
-## Basic Structure
-
-```yaml
-name: my-evaluation # Suite identifier
-description: Optional description of what this tests # Human-readable explanation
-dataset: path/to/dataset.jsonl # Test cases
-
-target: # What agent to evaluate
- kind: agent
- agent_file: agent.af # Agent to test
- base_url: http://localhost:8283 # Letta server
-
-graders: # How to evaluate responses
- my_metric:
- kind: tool # Deterministic grading
- function: exact_match # Grading function
- extractor: last_assistant # What to extract from agent response
-
-gate: # Pass/fail criteria
- metric_key: my_metric # Which metric to check
- op: gte # Greater than or equal
- value: 0.8 # 80% threshold
-```
-
-## Required Fields
-
-### name
-The name of your evaluation suite. Used in output and results.
-
-```yaml
-name: question-answering-eval
-```
-
-### dataset
-Path to the JSONL or CSV dataset file. Can be relative (to the suite YAML) or absolute.
-
-```yaml
-dataset: ./datasets/qa.jsonl # Relative to suite YAML location
-```
-
-### target
-Specifies what agent to evaluate. See [Targets](./targets.md) for details.
-
-### graders
-One or more graders to evaluate agent performance. See [Graders](./graders.md) for details.
-
-### gate
-Pass/fail criteria. See [Gates](./gates.md) for details.
-
-## Optional Fields
-
-### description
-A human-readable description of what this suite tests:
-
-```yaml
-description: Tests the agent's ability to answer factual questions accurately
-```
-
-### max_samples
-Limit the number of samples to evaluate (useful for quick tests):
-
-```yaml
-max_samples: 10 # Only evaluate first 10 samples
-```
-
-### sample_tags
-Filter samples by tags (only evaluate samples with these tags):
-
-```yaml
-sample_tags: [math, easy] # Only samples tagged with "math" AND "easy"
-```
-
-Dataset samples can include tags:
-```jsonl
-{"input": "What is 2+2?", "ground_truth": "4", "tags": ["math", "easy"]}
-```
-
-### num_runs
-Number of times to run the entire evaluation suite (useful for testing non-deterministic behavior):
-
-```yaml
-num_runs: 5 # Run the evaluation 5 times
-```
-
-Default: 1
-
-### setup_script
-Path to a Python script with a setup function to run before evaluation:
-
-```yaml
-setup_script: setup.py:prepare_environment # script.py:function_name
-```
-
-The setup function should have this signature:
-```python
-def prepare_environment(suite: SuiteSpec) -> None:
- # Setup code here
- pass
-```
-
-## Path Resolution
-
-Paths in the suite YAML are resolved relative to the YAML file location:
-
-```
-project/
-├── suite.yaml
-├── dataset.jsonl
-└── agents/
- └── my_agent.af
-```
-
-```yaml
-# In suite.yaml
-dataset: dataset.jsonl # Resolves to project/dataset.jsonl
-target:
- agent_file: agents/my_agent.af # Resolves to project/agents/my_agent.af
-```
-
-Absolute paths are used as-is.
-
-## Multi-Grader Suites
-
-You can evaluate multiple metrics in one suite:
-
-```yaml
-graders:
- accuracy: # Check if answer is correct
- kind: tool
- function: exact_match
- extractor: last_assistant
-
- completeness: # LLM judges response quality
- kind: rubric
- prompt_path: rubrics/completeness.txt
- model: gpt-4o-mini
- extractor: last_assistant
-
- tool_usage: # Verify correct tool was called
- kind: tool
- function: contains
- extractor: tool_arguments # Extract tool call arguments
-```
-
-The gate can check any of these metrics:
-
-```yaml
-gate:
- metric_key: accuracy # Gate on accuracy metric (others still computed)
- op: gte # Greater than or equal
- value: 0.9 # 90% threshold
-```
-
-Results will include scores for all graders, even if you only gate on one.
-
-## Examples
-
-### Simple Tool Grader Suite
-
-```yaml
-name: basic-qa # Suite name
-dataset: questions.jsonl # Test questions
-
-target:
- kind: agent
- agent_file: qa_agent.af # Pre-configured agent
- base_url: http://localhost:8283 # Local server
-
-graders:
- accuracy: # Single metric
- kind: tool # Deterministic grading
- function: contains # Check if ground truth is in response
- extractor: last_assistant # Use final agent message
-
-gate:
- metric_key: accuracy # Gate on this metric
- op: gte # Must be >=
- value: 0.75 # 75% to pass
-```
-
-### Rubric Grader Suite
-
-```yaml
-name: quality-eval # Quality evaluation
-dataset: prompts.jsonl # Test prompts
-
-target:
- kind: agent
- agent_id: existing-agent-123 # Use existing agent
- base_url: https://api.letta.com # Letta Cloud
-
-graders:
- quality: # LLM-as-judge metric
- kind: rubric # Subjective evaluation
- prompt_path: quality_rubric.txt # Rubric template
- model: gpt-4o-mini # Judge model
- temperature: 0.0 # Deterministic
- extractor: last_assistant # Evaluate final response
-
-gate:
- metric_key: quality # Gate on this metric
- metric: avg_score # Use average score
- op: gte # Must be >=
- value: 0.7 # 70% to pass
-```
-
-### Multi-Model Suite
-
-Test the same agent configuration across different models:
-
-```yaml
-name: model-comparison # Compare model performance
-dataset: test.jsonl # Same test for all models
-
-target:
- kind: agent
- agent_file: agent.af # Same agent configuration
- base_url: http://localhost:8283 # Local server
- model_configs: [gpt-4o-mini, claude-3-5-sonnet] # Test both models
-
-graders:
- accuracy: # Single metric for comparison
- kind: tool
- function: exact_match
- extractor: last_assistant
-
-gate:
- metric_key: accuracy # Both models must pass this
- op: gte # Must be >=
- value: 0.8 # 80% threshold
-```
-
-Results will show per-model metrics.
-
-## Validation
-
-Validate your suite configuration before running:
-
-```bash
-letta-evals validate suite.yaml
-```
-
-This checks:
-- Required fields are present
-- Paths exist
-- Configuration is valid
-- Grader/extractor combinations are compatible
-
-## Next Steps
-
-- [Dataset Configuration](./datasets.md)
-- [Target Configuration](./targets.md)
-- [Grader Configuration](./graders.md)
-- [Gate Configuration](./gates.md)
diff --git a/fern/pages/evals/concepts/suites.mdx b/fern/pages/evals/concepts/suites.mdx
deleted file mode 100644
index 95754986..00000000
--- a/fern/pages/evals/concepts/suites.mdx
+++ /dev/null
@@ -1,275 +0,0 @@
-# Suites
-
-A **suite** is a YAML configuration file that defines a complete evaluation specification. It's the central piece that ties together your dataset, target agent, grading criteria, and pass/fail thresholds.
-
-
-**Quick overview:**
-- **Single file defines everything**: Dataset, agent, graders, and success criteria all in one YAML
-- **Reusable and shareable**: Version control your evaluation specs alongside your code
-- **Multi-metric support**: Evaluate multiple aspects (accuracy, quality, tool usage) in one run
-- **Multi-model testing**: Run the same suite across different LLM models
-- **Flexible filtering**: Test subsets using tags or sample limits
-
-
-**Typical workflow:**
-1. Create a suite YAML defining what and how to test
-2. Run `letta-evals run suite.yaml`
-3. Review results showing scores for each metric
-4. Suite passes or fails based on gate criteria
-
-An evaluation suite is a YAML configuration file that defines a complete test specification.
-
-## Basic Structure
-
-```yaml
-name: my-evaluation # Suite identifier
-description: Optional description of what this tests # Human-readable explanation
-dataset: path/to/dataset.jsonl # Test cases
-
-target: # What agent to evaluate
- kind: agent
- agent_file: agent.af # Agent to test
- base_url: https://api.letta.com # Letta server
-
-graders: # How to evaluate responses
- my_metric:
- kind: tool # Deterministic grading
- function: exact_match # Grading function
- extractor: last_assistant # What to extract from agent response
-
-gate: # Pass/fail criteria
- metric_key: my_metric # Which metric to check
- op: gte # Greater than or equal
- value: 0.8 # 80% threshold
-```
-
-## Required Fields
-
-### name
-The name of your evaluation suite. Used in output and results.
-
-```yaml
-name: question-answering-eval
-```
-
-### dataset
-Path to the JSONL or CSV dataset file. Can be relative (to the suite YAML) or absolute.
-
-```yaml
-dataset: ./datasets/qa.jsonl # Relative to suite YAML location
-```
-
-### target
-Specifies what agent to evaluate. See [Targets](/guides/evals/concepts/targets) for details.
-
-### graders
-One or more graders to evaluate agent performance. See [Graders](/guides/evals/concepts/graders) for details.
-
-### gate
-Pass/fail criteria. See [Gates](/guides/evals/concepts/gates) for details.
-
-## Optional Fields
-
-### description
-A human-readable description of what this suite tests:
-
-```yaml
-description: Tests the agent's ability to answer factual questions accurately
-```
-
-### max_samples
-Limit the number of samples to evaluate (useful for quick tests):
-
-```yaml
-max_samples: 10 # Only evaluate first 10 samples
-```
-
-### sample_tags
-Filter samples by tags (only evaluate samples with these tags):
-
-```yaml
-sample_tags: [math, easy] # Only samples tagged with "math" AND "easy"
-```
-
-Dataset samples can include tags:
-```jsonl
-{"input": "What is 2+2?", "ground_truth": "4", "tags": ["math", "easy"]}
-```
-
-### num_runs
-Number of times to run the entire evaluation suite (useful for testing non-deterministic behavior):
-
-```yaml
-num_runs: 5 # Run the evaluation 5 times
-```
-
-Default: 1
-
-### setup_script
-Path to a Python script with a setup function to run before evaluation:
-
-```yaml
-setup_script: setup.py:prepare_environment # script.py:function_name
-```
-
-The setup function should have this signature:
-```python
-def prepare_environment(suite: SuiteSpec) -> None:
- # Setup code here
- pass
-```
-
-## Path Resolution
-
-Paths in the suite YAML are resolved relative to the YAML file location:
-
-```
-project/
-├── suite.yaml
-├── dataset.jsonl
-└── agents/
- └── my_agent.af
-```
-
-```yaml
-# In suite.yaml
-dataset: dataset.jsonl # Resolves to project/dataset.jsonl
-target:
- agent_file: agents/my_agent.af # Resolves to project/agents/my_agent.af
-```
-
-Absolute paths are used as-is.
-
-## Multi-Grader Suites
-
-You can evaluate multiple metrics in one suite:
-
-```yaml
-graders:
- accuracy: # Check if answer is correct
- kind: tool
- function: exact_match
- extractor: last_assistant
-
- completeness: # LLM judges response quality
- kind: rubric
- prompt_path: rubrics/completeness.txt
- model: gpt-4o-mini
- extractor: last_assistant
-
- tool_usage: # Verify correct tool was called
- kind: tool
- function: contains
- extractor: tool_arguments # Extract tool call arguments
-```
-
-The gate can check any of these metrics:
-
-```yaml
-gate:
- metric_key: accuracy # Gate on accuracy metric (others still computed)
- op: gte # Greater than or equal
- value: 0.9 # 90% threshold
-```
-
-Results will include scores for all graders, even if you only gate on one.
-
-## Examples
-
-### Simple Tool Grader Suite
-
-```yaml
-name: basic-qa # Suite name
-dataset: questions.jsonl # Test questions
-
-target:
- kind: agent
- agent_file: qa_agent.af # Pre-configured agent
- base_url: https://api.letta.com # Local server
-
-graders:
- accuracy: # Single metric
- kind: tool # Deterministic grading
- function: contains # Check if ground truth is in response
- extractor: last_assistant # Use final agent message
-
-gate:
- metric_key: accuracy # Gate on this metric
- op: gte # Must be >=
- value: 0.75 # 75% to pass
-```
-
-### Rubric Grader Suite
-
-```yaml
-name: quality-eval # Quality evaluation
-dataset: prompts.jsonl # Test prompts
-
-target:
- kind: agent
- agent_id: existing-agent-123 # Use existing agent
- base_url: https://api.letta.com # Letta Cloud
-
-graders:
- quality: # LLM-as-judge metric
- kind: rubric # Subjective evaluation
- prompt_path: quality_rubric.txt # Rubric template
- model: gpt-4o-mini # Judge model
- temperature: 0.0 # Deterministic
- extractor: last_assistant # Evaluate final response
-
-gate:
- metric_key: quality # Gate on this metric
- metric: avg_score # Use average score
- op: gte # Must be >=
- value: 0.7 # 70% to pass
-```
-
-### Multi-Model Suite
-
-Test the same agent configuration across different models:
-
-```yaml
-name: model-comparison # Compare model performance
-dataset: test.jsonl # Same test for all models
-
-target:
- kind: agent
- agent_file: agent.af # Same agent configuration
- base_url: https://api.letta.com # Local server
- model_configs: [gpt-4o-mini, claude-3-5-sonnet] # Test both models
-
-graders:
- accuracy: # Single metric for comparison
- kind: tool
- function: exact_match
- extractor: last_assistant
-
-gate:
- metric_key: accuracy # Both models must pass this
- op: gte # Must be >=
- value: 0.8 # 80% threshold
-```
-
-Results will show per-model metrics.
-
-## Validation
-
-Validate your suite configuration before running:
-
-```bash
-letta-evals validate suite.yaml
-```
-
-This checks:
-- Required fields are present
-- Paths exist
-- Configuration is valid
-- Grader/extractor combinations are compatible
-
-## Next Steps
-
-- [Dataset Configuration](/guides/evals/concepts/datasets)
-- [Target Configuration](/guides/evals/concepts/targets)
-- [Grader Configuration](/guides/evals/concepts/graders)
-- [Gate Configuration](/guides/evals/concepts/gates)
diff --git a/fern/pages/evals/concepts/targets.md b/fern/pages/evals/concepts/targets.md
deleted file mode 100644
index 61aa9948..00000000
--- a/fern/pages/evals/concepts/targets.md
+++ /dev/null
@@ -1,319 +0,0 @@
-# Targets
-
-A **target** is the agent you're evaluating. In Letta Evals, the target configuration determines how agents are created, accessed, and tested.
-
-**Quick overview:**
-- **Three ways to specify agents**: agent file (`.af`), existing agent ID, or programmatic creation script
-- **Critical distinction**: `agent_file`/`agent_script` create fresh agents per sample (isolated tests), while `agent_id` uses one agent for all samples (stateful conversation)
-- **Multi-model support**: Test the same agent configuration across different LLM models
-- **Flexible connection**: Connect to local Letta servers or Letta Cloud
-
-**When to use each approach:**
-- `agent_file` - Pre-configured agents saved as `.af` files (most common)
-- `agent_id` - Testing existing agents or multi-turn conversations with state
-- `agent_script` - Dynamic agent creation with per-sample customization
-
-The target configuration specifies how to create or access the agent for evaluation.
-
-## Target Configuration
-
-All targets have a `kind` field (currently only `agent` is supported):
-
-```yaml
-target:
- kind: agent # Currently only "agent" is supported
- # ... agent-specific configuration
-```
-
-## Agent Sources
-
-You must specify exactly ONE of these:
-
-### agent_file
-
-Path to a `.af` (Agent File) to upload:
-
-```yaml
-target:
- kind: agent
- agent_file: path/to/agent.af # Path to .af file
- base_url: http://localhost:8283 # Letta server URL
-```
-
-The agent file will be uploaded to the Letta server and a new agent created for the evaluation.
-
-### agent_id
-
-ID of an existing agent on the server:
-
-```yaml
-target:
- kind: agent
- agent_id: agent-123-abc # ID of existing agent
- base_url: http://localhost:8283 # Letta server URL
-```
-
-The existing agent will be used directly. Note: this agent's memory will be modified during evaluation.
-
-### agent_script
-
-Path to a Python script with an agent factory function for programmatic agent creation:
-
-```yaml
-target:
- kind: agent
- agent_script: create_agent.py:create_inventory_agent # script.py:function_name
- base_url: http://localhost:8283 # Letta server URL
-```
-
-Format: `path/to/script.py:function_name`
-
-The function must be decorated with `@agent_factory` and have the signature `async (client: AsyncLetta, sample: Sample) -> str`:
-
-```python
-from letta_client import AsyncLetta, CreateBlock
-from letta_evals.decorators import agent_factory
-from letta_evals.models import Sample
-
-@agent_factory
-async def create_inventory_agent(client: AsyncLetta, sample: Sample) -> str:
- """Create and return agent ID for this sample."""
- # Access custom arguments from the dataset
- item = sample.agent_args.get("item", {})
-
- # Create agent with sample-specific configuration
- agent = await client.agents.create(
- name="inventory-assistant",
- memory_blocks=[
- CreateBlock(
- label="item_context",
- value=f"Item: {item.get('name', 'Unknown')}"
- )
- ],
- agent_type="letta_v1_agent",
- model="openai/gpt-4.1-mini",
- embedding="openai/text-embedding-3-small",
- )
-
- return agent.id
-```
-
-**Key features:**
-- Creates a fresh agent for each sample
-- Can customize agents using `sample.agent_args` from the dataset
-- Allows testing agent creation logic itself
-- Useful when you don't have pre-saved agent files
-
-**When to use:**
-- Testing agent creation workflows
-- Dynamic per-sample agent configuration
-- Agents that need sample-specific memory or tools
-- Programmatic agent testing
-
-See [`examples/programmatic-agent-creation/`](https://github.com/letta-ai/letta-evals/tree/main/examples/programmatic-agent-creation) for a complete working example.
-
-## Connection Configuration
-
-### base_url
-
-Letta server URL:
-
-```yaml
-target:
- base_url: http://localhost:8283 # Local Letta server
- # or
- base_url: https://api.letta.com # Letta Cloud
-```
-
-Default: `http://localhost:8283`
-
-### api_key
-
-API key for authentication (required for Letta Cloud):
-
-```yaml
-target:
- api_key: your-api-key-here # Required for Letta Cloud
-```
-
-Or set via environment variable:
-```bash
-export LETTA_API_KEY=your-api-key-here
-```
-
-### project_id
-
-Letta project ID (for Letta Cloud):
-
-```yaml
-target:
- project_id: proj_abc123 # Letta Cloud project
-```
-
-Or set via environment variable:
-```bash
-export LETTA_PROJECT_ID=proj_abc123
-```
-
-### timeout
-
-Request timeout in seconds:
-
-```yaml
-target:
- timeout: 300.0 # Request timeout (5 minutes)
-```
-
-Default: 300 seconds
-
-## Multi-Model Evaluation
-
-Test the same agent across different models:
-
-### model_configs
-
-List of model configuration names from JSON files:
-
-```yaml
-target:
- kind: agent
- agent_file: agent.af
- model_configs: [gpt-4o-mini, claude-3-5-sonnet] # Test with both models
-```
-
-The evaluation will run once for each model config. Model configs are JSON files in `letta_evals/llm_model_configs/`.
-
-### model_handles
-
-List of model handles (cloud-compatible identifiers):
-
-```yaml
-target:
- kind: agent
- agent_file: agent.af
- model_handles: ["openai/gpt-4o-mini", "anthropic/claude-3-5-sonnet"] # Cloud model identifiers
-```
-
-Use this for Letta Cloud deployments.
-
-**Note**: You cannot specify both `model_configs` and `model_handles`.
-
-## Complete Examples
-
-### Local Development
-
-```yaml
-target:
- kind: agent
- agent_file: ./agents/my_agent.af # Pre-configured agent
- base_url: http://localhost:8283 # Local server
-```
-
-### Letta Cloud
-
-```yaml
-target:
- kind: agent
- agent_id: agent-cloud-123 # Existing cloud agent
- base_url: https://api.letta.com # Letta Cloud
- api_key: ${LETTA_API_KEY} # From environment variable
- project_id: proj_abc # Your project ID
-```
-
-### Multi-Model Testing
-
-```yaml
-target:
- kind: agent
- agent_file: agent.af # Same agent configuration
- base_url: http://localhost:8283 # Local server
- model_configs: [gpt-4o-mini, gpt-4o, claude-3-5-sonnet] # Test 3 models
-```
-
-Results will include per-model metrics:
-```
-Model: gpt-4o-mini - Avg: 0.85, Pass: 85.0%
-Model: gpt-4o - Avg: 0.92, Pass: 92.0%
-Model: claude-3-5-sonnet - Avg: 0.88, Pass: 88.0%
-```
-
-### Programmatic Agent Creation
-
-```yaml
-target:
- kind: agent
- agent_script: setup.py:CustomAgentFactory # Programmatic creation
- base_url: http://localhost:8283 # Local server
-```
-
-## Environment Variable Precedence
-
-Configuration values are resolved in this order (highest priority first):
-
-1. CLI arguments (`--api-key`, `--base-url`, `--project-id`)
-2. Suite YAML configuration
-3. Environment variables (`LETTA_API_KEY`, `LETTA_BASE_URL`, `LETTA_PROJECT_ID`)
-
-## Agent Lifecycle and Testing Behavior
-
-The way your agent is specified fundamentally changes how the evaluation runs:
-
-### With agent_file or agent_script: Independent Testing
-
-**Agent lifecycle:**
-1. A fresh agent instance is created for each sample
-2. Agent processes the sample input(s)
-3. Agent remains on the server after the sample completes
-
-**Testing behavior:** Each sample is an independent, isolated test. Agent state (memory, message history) does not carry over between samples. This enables parallel execution and ensures reproducible results.
-
-**Use cases:**
-- Testing how the agent responds to various independent inputs
-- Ensuring consistent behavior across different scenarios
-- Regression testing where each case should be isolated
-- Evaluating agent responses without prior context
-
-**Example:** If you have 10 test cases, 10 separate agent instances will be created (one per test case), and they can run in parallel.
-
-### With agent_id: Sequential Script Testing
-
-**Agent lifecycle:**
-1. The same agent instance is used for all samples
-2. Agent processes each sample in sequence
-3. Agent state persists throughout the entire evaluation
-
-**Testing behavior:** The dataset becomes a conversation script where each sample builds on previous ones. Agent memory and message history accumulate, and earlier interactions affect later responses. Samples must execute sequentially.
-
-**Use cases:**
-- Testing multi-turn conversations with context
-- Evaluating how agent memory evolves over time
-- Simulating a single user session with multiple interactions
-- Testing scenarios where context should accumulate
-
-**Example:** If you have 10 test cases, they all run against the same agent instance in order, with state carrying over between each test.
-
-### Critical Differences
-
-| Aspect | agent_file / agent_script | agent_id |
-|--------|---------------------------|----------|
-| **Agent instances** | New agent per sample | Same agent for all samples |
-| **State isolation** | Fully isolated | State carries over |
-| **Execution** | Can run in parallel | Must run sequentially |
-| **Memory** | Fresh for each sample | Accumulates across samples |
-| **Use case** | Independent test cases | Conversation scripts |
-| **Reproducibility** | Highly reproducible | Depends on execution order |
-
-**Best practice:** Use `agent_file` or `agent_script` for most evaluations to ensure reproducible, isolated tests. Use `agent_id` only when you specifically need to test how agent state evolves across multiple interactions.
-
-## Validation
-
-The runner validates:
-- Exactly one of `agent_file`, `agent_id`, or `agent_script` is specified
-- Agent files have `.af` extension
-- Agent script paths are valid
-
-## Next Steps
-
-- [Suite YAML Reference](../configuration/suite-yaml.md) - Complete target configuration options
-- [Datasets](./datasets.md) - Using agent_args for sample-specific configuration
-- [Getting Started](../getting-started.md) - Complete tutorial with target examples
diff --git a/fern/pages/evals/concepts/targets.mdx b/fern/pages/evals/concepts/targets.mdx
deleted file mode 100644
index 0b52b28b..00000000
--- a/fern/pages/evals/concepts/targets.mdx
+++ /dev/null
@@ -1,329 +0,0 @@
-# Targets
-
-A **target** is the agent you're evaluating. In Letta Evals, the target configuration determines how agents are created, accessed, and tested.
-
-
-**Quick overview:**
-- **Three ways to specify agents**: agent file (`.af`), existing agent ID, or programmatic creation script
-- **Critical distinction**: `agent_file`/`agent_script` create fresh agents per sample (isolated tests), while `agent_id` uses one agent for all samples (stateful conversation)
-- **Multi-model support**: Test the same agent configuration across different LLM models
-- **Flexible connection**: Connect to local Letta servers or Letta Cloud
-
-
-**When to use each approach:**
-- `agent_file` - Pre-configured agents saved as `.af` files (most common)
-- `agent_id` - Testing existing agents or multi-turn conversations with state
-- `agent_script` - Dynamic agent creation with per-sample customization
-
-The target configuration specifies how to create or access the agent for evaluation.
-
-## Target Configuration
-
-All targets have a `kind` field (currently only `agent` is supported):
-
-```yaml
-target:
- kind: agent # Currently only "agent" is supported
- # ... agent-specific configuration
-```
-
-## Agent Sources
-
-You must specify exactly ONE of these:
-
-### agent_file
-
-Path to a `.af` (Agent File) to upload:
-
-```yaml
-target:
- kind: agent
- agent_file: path/to/agent.af # Path to .af file
- base_url: https://api.letta.com # Letta server URL
-```
-
-The agent file will be uploaded to the Letta server and a new agent created for the evaluation.
-
-### agent_id
-
-ID of an existing agent on the server:
-
-```yaml
-target:
- kind: agent
- agent_id: agent-123-abc # ID of existing agent
- base_url: https://api.letta.com # Letta server URL
-```
-
-
-**Modifies agent in-place:** Using `agent_id` will modify your agent's state, memory, and message history during evaluation. The same agent instance is used for all samples, processing them sequentially. **Do not use production agents or agents you don't want to modify.** Use `agent_file` or `agent_script` for reproducible, isolated testing.
-
-
-### agent_script
-
-Path to a Python script with an agent factory function for programmatic agent creation:
-
-```yaml
-target:
- kind: agent
- agent_script: create_agent.py:create_inventory_agent # script.py:function_name
- base_url: https://api.letta.com # Letta server URL
-```
-
-Format: `path/to/script.py:function_name`
-
-The function must be decorated with `@agent_factory` and have the signature `async (client: AsyncLetta, sample: Sample) -> str`:
-
-```python
-from letta_client import AsyncLetta, CreateBlock
-from letta_evals.decorators import agent_factory
-from letta_evals.models import Sample
-
-@agent_factory
-async def create_inventory_agent(client: AsyncLetta, sample: Sample) -> str:
- """Create and return agent ID for this sample."""
- # Access custom arguments from the dataset
- item = sample.agent_args.get("item", {})
-
- # Create agent with sample-specific configuration
- agent = await client.agents.create(
- name="inventory-assistant",
- memory_blocks=[
- CreateBlock(
- label="item_context",
- value=f"Item: {item.get('name', 'Unknown')}"
- )
- ],
- agent_type="letta_v1_agent",
- model="openai/gpt-4.1-mini",
- embedding="openai/text-embedding-3-small",
- )
-
- return agent.id
-```
-
-**Key features:**
-- Creates a fresh agent for each sample
-- Can customize agents using `sample.agent_args` from the dataset
-- Allows testing agent creation logic itself
-- Useful when you don't have pre-saved agent files
-
-**When to use:**
-- Testing agent creation workflows
-- Dynamic per-sample agent configuration
-- Agents that need sample-specific memory or tools
-- Programmatic agent testing
-
-## Connection Configuration
-
-### base_url
-
-Letta server URL:
-
-```yaml
-target:
- base_url: https://api.letta.com # Local Letta server
- # or
- base_url: https://api.letta.com # Letta Cloud
-```
-
-Default: `https://api.letta.com`
-
-### api_key
-
-API key for authentication (required for Letta Cloud):
-
-```yaml
-target:
- api_key: your-api-key-here # Required for Letta Cloud
-```
-
-Or set via environment variable:
-```bash
-export LETTA_API_KEY=your-api-key-here
-```
-
-### project_id
-
-Letta project ID (for Letta Cloud):
-
-```yaml
-target:
- project_id: proj_abc123 # Letta Cloud project
-```
-
-Or set via environment variable:
-```bash
-export LETTA_PROJECT_ID=proj_abc123
-```
-
-### timeout
-
-Request timeout in seconds:
-
-```yaml
-target:
- timeout: 300.0 # Request timeout (5 minutes)
-```
-
-Default: 300 seconds
-
-## Multi-Model Evaluation
-
-Test the same agent across different models:
-
-### model_configs
-
-List of model configuration names from JSON files:
-
-```yaml
-target:
- kind: agent
- agent_file: agent.af
- model_configs: [gpt-4o-mini, claude-3-5-sonnet] # Test with both models
-```
-
-The evaluation will run once for each model config. Model configs are JSON files in `letta_evals/llm_model_configs/`.
-
-### model_handles
-
-List of model handles (cloud-compatible identifiers):
-
-```yaml
-target:
- kind: agent
- agent_file: agent.af
- model_handles: ["openai/gpt-4o-mini", "anthropic/claude-3-5-sonnet"] # Cloud model identifiers
-```
-
-Use this for Letta Cloud deployments.
-
-
-**Note**: You cannot specify both `model_configs` and `model_handles`.
-
-
-## Complete Examples
-
-### Local Development
-
-```yaml
-target:
- kind: agent
- agent_file: ./agents/my_agent.af # Pre-configured agent
- base_url: https://api.letta.com # Local server
-```
-
-### Letta Cloud
-
-```yaml
-target:
- kind: agent
- agent_id: agent-cloud-123 # Existing cloud agent
- base_url: https://api.letta.com # Letta Cloud
- api_key: ${LETTA_API_KEY} # From environment variable
- project_id: proj_abc # Your project ID
-```
-
-### Multi-Model Testing
-
-```yaml
-target:
- kind: agent
- agent_file: agent.af # Same agent configuration
- base_url: https://api.letta.com # Local server
- model_configs: [gpt-4o-mini, gpt-4o, claude-3-5-sonnet] # Test 3 models
-```
-
-Results will include per-model metrics:
-```
-Model: gpt-4o-mini - Avg: 0.85, Pass: 85.0%
-Model: gpt-4o - Avg: 0.92, Pass: 92.0%
-Model: claude-3-5-sonnet - Avg: 0.88, Pass: 88.0%
-```
-
-### Programmatic Agent Creation
-
-```yaml
-target:
- kind: agent
- agent_script: setup.py:CustomAgentFactory # Programmatic creation
- base_url: https://api.letta.com # Local server
-```
-
-## Environment Variable Precedence
-
-Configuration values are resolved in this order (highest priority first):
-
-1. CLI arguments (`--api-key`, `--base-url`, `--project-id`)
-2. Suite YAML configuration
-3. Environment variables (`LETTA_API_KEY`, `LETTA_BASE_URL`, `LETTA_PROJECT_ID`)
-
-## Agent Lifecycle and Testing Behavior
-
-The way your agent is specified fundamentally changes how the evaluation runs:
-
-### With agent_file or agent_script: Independent Testing
-
-**Agent lifecycle:**
-1. A fresh agent instance is created for each sample
-2. Agent processes the sample input(s)
-3. Agent remains on the server after the sample completes
-
-**Testing behavior:** Each sample is an independent, isolated test. Agent state (memory, message history) does not carry over between samples. This enables parallel execution and ensures reproducible results.
-
-**Use cases:**
-- Testing how the agent responds to various independent inputs
-- Ensuring consistent behavior across different scenarios
-- Regression testing where each case should be isolated
-- Evaluating agent responses without prior context
-
-
-**Example:** If you have 10 test cases, 10 separate agent instances will be created (one per test case), and they can run in parallel.
-
-
-### With agent_id: Sequential Script Testing
-
-**Agent lifecycle:**
-1. The same agent instance is used for all samples
-2. Agent processes each sample in sequence
-3. Agent state persists throughout the entire evaluation
-
-**Testing behavior:** The dataset becomes a conversation script where each sample builds on previous ones. Agent memory and message history accumulate, and earlier interactions affect later responses. Samples must execute sequentially.
-
-**Use cases:**
-- Testing multi-turn conversations with context
-- Evaluating how agent memory evolves over time
-- Simulating a single user session with multiple interactions
-- Testing scenarios where context should accumulate
-
-
-**Example:** If you have 10 test cases, they all run against the same agent instance in order, with state carrying over between each test.
-
-
-### Critical Differences
-
-| Aspect | agent_file / agent_script | agent_id |
-|--------|---------------------------|----------|
-| **Agent instances** | New agent per sample | Same agent for all samples |
-| **State isolation** | Fully isolated | State carries over |
-| **Execution** | Can run in parallel | Must run sequentially |
-| **Memory** | Fresh for each sample | Accumulates across samples |
-| **Use case** | Independent test cases | Conversation scripts |
-| **Reproducibility** | Highly reproducible | Depends on execution order |
-
-
-**Best practice:** Use `agent_file` or `agent_script` for most evaluations to ensure reproducible, isolated tests. Use `agent_id` only when you specifically need to test how agent state evolves across multiple interactions.
-
-
-## Validation
-
-The runner validates:
-- Exactly one of `agent_file`, `agent_id`, or `agent_script` is specified
-- Agent files have `.af` extension
-- Agent script paths are valid
-
-## Next Steps
-
-- [Suite YAML Reference](/guides/evals/configuration/suite-yaml) - Complete target configuration options
-- [Datasets](/guides/evals/concepts/datasets) - Using agent_args for sample-specific configuration
-- [Getting Started](/guides/evals/getting-started) - Complete tutorial with target examples
diff --git a/fern/pages/evals/configuration/suite-yaml.md b/fern/pages/evals/configuration/suite-yaml.md
deleted file mode 100644
index 8b151e77..00000000
--- a/fern/pages/evals/configuration/suite-yaml.md
+++ /dev/null
@@ -1,783 +0,0 @@
-# Suite YAML Reference
-
-Complete reference for suite configuration files.
-
-A **suite** is a YAML file that defines an evaluation: what agent to test, what dataset to use, how to grade responses, and what criteria determine pass/fail. This is your evaluation specification.
-
-**Quick overview:**
-- **name**: Identifier for your evaluation
-- **dataset**: JSONL file with test cases
-- **target**: Which agent to evaluate (via file, ID, or script)
-- **graders**: How to score responses (tool or rubric graders)
-- **gate**: Pass/fail criteria
-
-See [Getting Started](../getting-started.md) for a tutorial, or [Core Concepts](../concepts/suites.md) for conceptual overview.
-
-## File Structure
-
-```yaml
-name: string (required)
-description: string (optional)
-dataset: path (required)
-max_samples: integer (optional)
-sample_tags: array (optional)
-num_runs: integer (optional)
-setup_script: string (optional)
-
-target: object (required)
- kind: "agent"
- base_url: string
- api_key: string
- timeout: float
- project_id: string
- agent_id: string (one of: agent_id, agent_file, agent_script)
- agent_file: path
- agent_script: string
- model_configs: array
- model_handles: array
-
-graders: object (required)
- : object
- kind: "tool" | "rubric"
- display_name: string
- extractor: string
- extractor_config: object
- # Tool grader fields
- function: string
- # Rubric grader fields (LLM API)
- prompt: string
- prompt_path: path
- model: string
- temperature: float
- provider: string
- max_retries: integer
- timeout: float
- rubric_vars: array
- # Rubric grader fields (agent-as-judge)
- agent_file: path
- judge_tool_name: string
-
-gate: object (required)
- metric_key: string
- metric: "avg_score" | "accuracy"
- op: "gte" | "gt" | "lte" | "lt" | "eq"
- value: float
- pass_op: "gte" | "gt" | "lte" | "lt" | "eq"
- pass_value: float
-```
-
-## Top-Level Fields
-
-### name (required)
-
-Suite name, used in output and results.
-
-**Type**: string
-
-**Example**:
-```yaml
-name: question-answering-eval
-```
-
-### description (optional)
-
-Human-readable description of what the suite tests.
-
-**Type**: string
-
-**Example**:
-```yaml
-description: Tests agent's ability to answer factual questions accurately
-```
-
-### dataset (required)
-
-Path to JSONL dataset file. Relative paths are resolved from the suite YAML location.
-
-**Type**: path (string)
-
-**Example**:
-```yaml
-dataset: ./datasets/qa.jsonl
-dataset: /absolute/path/to/dataset.jsonl
-```
-
-### max_samples (optional)
-
-Limit the number of samples to evaluate. Useful for quick tests.
-
-**Type**: integer
-
-**Default**: All samples
-
-**Example**:
-```yaml
-max_samples: 10 # Only evaluate first 10 samples
-```
-
-### sample_tags (optional)
-
-Filter samples by tags. Only samples with ALL specified tags are evaluated.
-
-**Type**: array of strings
-
-**Example**:
-```yaml
-sample_tags: [math, easy] # Only samples tagged with both
-```
-
-Dataset samples need tags:
-```jsonl
-{"input": "What is 2+2?", "ground_truth": "4", "tags": ["math", "easy"]}
-```
-
-### num_runs (optional)
-
-Number of times to run the evaluation suite. Useful for testing non-deterministic behavior or collecting multiple runs for statistical analysis.
-
-**Type**: integer
-
-**Default**: 1
-
-**Example**:
-```yaml
-num_runs: 5 # Run the evaluation 5 times
-```
-
-### setup_script (optional)
-
-Path to Python script with setup function.
-
-**Type**: string (format: `path/to/script.py:function_name`)
-
-**Example**:
-```yaml
-setup_script: setup.py:prepare_environment
-```
-
-The function signature:
-```python
-def prepare_environment(suite: SuiteSpec) -> None:
- # Setup code
- pass
-```
-
-## target (required)
-
-Configuration for the agent being evaluated.
-
-### kind (required)
-
-Type of target. Currently only `"agent"` is supported.
-
-**Type**: string
-
-**Example**:
-```yaml
-target:
- kind: agent
-```
-
-### base_url (optional)
-
-Letta server URL.
-
-**Type**: string
-
-**Default**: `http://localhost:8283`
-
-**Example**:
-```yaml
-target:
- base_url: http://localhost:8283
- # or
- base_url: https://api.letta.com
-```
-
-### api_key (optional)
-
-API key for Letta authentication. Can also be set via `LETTA_API_KEY` environment variable.
-
-**Type**: string
-
-**Example**:
-```yaml
-target:
- api_key: your-api-key-here
-```
-
-### timeout (optional)
-
-Request timeout in seconds.
-
-**Type**: float
-
-**Default**: 300.0
-
-**Example**:
-```yaml
-target:
- timeout: 600.0 # 10 minutes
-```
-
-### project_id (optional)
-
-Letta project ID (for Letta Cloud).
-
-**Type**: string
-
-**Example**:
-```yaml
-target:
- project_id: proj_abc123
-```
-
-### Agent Source (required, pick one)
-
-Exactly one of these must be specified:
-
-#### agent_id
-
-ID of existing agent on the server.
-
-**Type**: string
-
-**Example**:
-```yaml
-target:
- agent_id: agent-123-abc
-```
-
-#### agent_file
-
-Path to `.af` agent file.
-
-**Type**: path (string, must end in `.af`)
-
-**Example**:
-```yaml
-target:
- agent_file: ./agents/my_agent.af
-```
-
-#### agent_script
-
-Path to Python script with agent factory.
-
-**Type**: string (format: `path/to/script.py:ClassName`)
-
-**Example**:
-```yaml
-target:
- agent_script: factory.py:MyAgentFactory
-```
-
-See [Targets](../concepts/targets.md) for details on agent sources.
-
-### model_configs (optional)
-
-List of model configuration names to test. Cannot be used with `model_handles`.
-
-**Type**: array of strings
-
-**Example**:
-```yaml
-target:
- model_configs: [gpt-4o-mini, claude-3-5-sonnet]
-```
-
-### model_handles (optional)
-
-List of model handles for cloud deployments. Cannot be used with `model_configs`.
-
-**Type**: array of strings
-
-**Example**:
-```yaml
-target:
- model_handles: ["openai/gpt-4o-mini", "anthropic/claude-3-5-sonnet"]
-```
-
-## graders (required)
-
-One or more graders, each with a unique key.
-
-### Grader Key
-
-The key becomes the metric name:
-
-```yaml
-graders:
- accuracy: # This is the metric_key
- kind: tool
- ...
- quality: # Another metric_key
- kind: rubric
- ...
-```
-
-### kind (required)
-
-Grader type: `"tool"` or `"rubric"`.
-
-**Type**: string
-
-**Example**:
-```yaml
-graders:
- my_metric:
- kind: tool
-```
-
-### display_name (optional)
-
-Human-friendly name for CLI/UI output.
-
-**Type**: string
-
-**Example**:
-```yaml
-graders:
- acc:
- display_name: "Answer Accuracy"
- kind: tool
- ...
-```
-
-### extractor (required)
-
-Name of the extractor to use.
-
-**Type**: string
-
-**Example**:
-```yaml
-graders:
- my_metric:
- extractor: last_assistant
-```
-
-### extractor_config (optional)
-
-Configuration passed to the extractor.
-
-**Type**: object
-
-**Example**:
-```yaml
-graders:
- my_metric:
- extractor: pattern
- extractor_config:
- pattern: 'Answer: (.*)'
- group: 1
-```
-
-### Tool Grader Fields
-
-#### function (required for tool graders)
-
-Name of the grading function.
-
-**Type**: string
-
-**Example**:
-```yaml
-graders:
- accuracy:
- kind: tool
- function: exact_match
-```
-
-### Rubric Grader Fields
-
-#### prompt (required if no prompt_path)
-
-Inline rubric prompt.
-
-**Type**: string
-
-**Example**:
-```yaml
-graders:
- quality:
- kind: rubric
- prompt: |
- Evaluate response quality from 0.0 to 1.0.
- Input: {input}
- Response: {submission}
-```
-
-#### prompt_path (required if no prompt)
-
-Path to rubric file. Cannot use both `prompt` and `prompt_path`.
-
-**Type**: path (string)
-
-**Example**:
-```yaml
-graders:
- quality:
- kind: rubric
- prompt_path: rubrics/quality.txt
-```
-
-#### model (optional)
-
-LLM model for judging.
-
-**Type**: string
-
-**Default**: `gpt-4o-mini`
-
-**Example**:
-```yaml
-graders:
- quality:
- kind: rubric
- model: gpt-4o
-```
-
-#### temperature (optional)
-
-Temperature for LLM generation.
-
-**Type**: float (0.0 to 2.0)
-
-**Default**: 0.0
-
-**Example**:
-```yaml
-graders:
- quality:
- kind: rubric
- temperature: 0.0
-```
-
-#### provider (optional)
-
-LLM provider.
-
-**Type**: string
-
-**Default**: `openai`
-
-**Example**:
-```yaml
-graders:
- quality:
- kind: rubric
- provider: openai
-```
-
-#### max_retries (optional)
-
-Maximum retry attempts for API calls.
-
-**Type**: integer
-
-**Default**: 5
-
-**Example**:
-```yaml
-graders:
- quality:
- kind: rubric
- max_retries: 3
-```
-
-#### timeout (optional)
-
-Timeout for API calls in seconds.
-
-**Type**: float
-
-**Default**: 120.0
-
-**Example**:
-```yaml
-graders:
- quality:
- kind: rubric
- timeout: 60.0
-```
-
-#### rubric_vars (optional)
-
-List of custom variable names that must be provided in the dataset for rubric template substitution. When specified, the grader validates that each sample includes these variables in its `rubric_vars` field.
-
-**Type**: array of strings
-
-**Example**:
-```yaml
-graders:
- code_quality:
- kind: rubric
- rubric_vars: [reference_code, required_features] # Require these variables in dataset
- prompt: |
- Compare the submission to this reference:
- {reference_code}
-
- Required features: {required_features}
-```
-
-Dataset sample must provide these variables:
-```jsonl
-{"input": "Write a fibonacci function", "rubric_vars": {"reference_code": "def fib(n):\n if n <= 1: return n\n return fib(n-1) + fib(n-2)", "required_features": "recursion, base case"}}
-```
-
-See [Datasets - rubric_vars](../concepts/datasets.md#rubric_vars) for details.
-
-#### agent_file (required for agent-as-judge)
-
-Path to `.af` agent file to use as judge for rubric grading. Use this instead of `model` when you want a Letta agent to act as the evaluator.
-
-**Type**: path (string)
-
-**Mutually exclusive with**: `model`, `temperature`, `provider`, `max_retries`, `timeout`
-
-**Example**:
-```yaml
-graders:
- agent_judge:
- kind: rubric
- agent_file: judge.af # Judge agent with submit_grade tool
- prompt_path: rubric.txt # Evaluation criteria
- extractor: last_assistant
-```
-
-**Requirements**: The judge agent must have a tool with signature `submit_grade(score: float, rationale: str)`. The framework validates this on initialization.
-
-See [Rubric Graders - Agent-as-Judge](../graders/rubric-graders.md#agent-as-judge) for complete documentation.
-
-#### judge_tool_name (optional, for agent-as-judge)
-
-Name of the tool that the judge agent uses to submit scores. Only applicable when using `agent_file`.
-
-**Type**: string
-
-**Default**: `submit_grade`
-
-**Example**:
-```yaml
-graders:
- agent_judge:
- kind: rubric
- agent_file: judge.af
- judge_tool_name: submit_grade # Default, can be omitted
- prompt_path: rubric.txt
- extractor: last_assistant
-```
-
-**Tool requirements**: The tool must have exactly two parameters:
-- `score: float` - Score between 0.0 and 1.0
-- `rationale: str` - Explanation of the score
-
-## gate (required)
-
-Pass/fail criteria for the evaluation.
-
-### metric_key (optional)
-
-Which grader to evaluate. If only one grader, this can be omitted.
-
-**Type**: string
-
-**Example**:
-```yaml
-gate:
- metric_key: accuracy # Must match a key in graders
-```
-
-### metric (optional)
-
-Which aggregate to compare: `avg_score` or `accuracy`.
-
-**Type**: string
-
-**Default**: `avg_score`
-
-**Example**:
-```yaml
-gate:
- metric: avg_score
- # or
- metric: accuracy
-```
-
-### op (required)
-
-Comparison operator.
-
-**Type**: string (one of: `gte`, `gt`, `lte`, `lt`, `eq`)
-
-**Example**:
-```yaml
-gate:
- op: gte # Greater than or equal
-```
-
-### value (required)
-
-Threshold value for comparison.
-
-**Type**: float (0.0 to 1.0)
-
-**Example**:
-```yaml
-gate:
- value: 0.8 # Require >= 0.8
-```
-
-### pass_op (optional)
-
-Comparison operator for per-sample pass criteria.
-
-**Type**: string (one of: `gte`, `gt`, `lte`, `lt`, `eq`)
-
-**Default**: Same as `op`
-
-**Example**:
-```yaml
-gate:
- metric: accuracy
- pass_op: gte # Sample passes if...
- pass_value: 0.7 # ...score >= 0.7
-```
-
-### pass_value (optional)
-
-Threshold for per-sample pass.
-
-**Type**: float (0.0 to 1.0)
-
-**Default**: Same as `value` (or 1.0 for accuracy metric)
-
-**Example**:
-```yaml
-gate:
- metric: accuracy
- op: gte
- value: 0.8 # 80% must pass
- pass_op: gte
- pass_value: 0.7 # Sample passes if score >= 0.7
-```
-
-## Complete Examples
-
-### Minimal Suite
-
-```yaml
-name: basic-eval
-dataset: dataset.jsonl
-
-target:
- kind: agent
- agent_file: agent.af
-
-graders:
- accuracy:
- kind: tool
- function: exact_match
- extractor: last_assistant
-
-gate:
- op: gte
- value: 0.8
-```
-
-### Multi-Metric Suite
-
-```yaml
-name: comprehensive-eval
-description: Tests accuracy and quality
-dataset: test_data.jsonl
-max_samples: 100
-
-target:
- kind: agent
- agent_file: agent.af
- base_url: http://localhost:8283
-
-graders:
- accuracy:
- display_name: "Answer Accuracy"
- kind: tool
- function: contains
- extractor: last_assistant
-
- quality:
- display_name: "Response Quality"
- kind: rubric
- prompt_path: rubrics/quality.txt
- model: gpt-4o-mini
- temperature: 0.0
- extractor: last_assistant
-
-gate:
- metric_key: accuracy
- metric: avg_score
- op: gte
- value: 0.85
-```
-
-### Advanced Suite
-
-```yaml
-name: advanced-eval
-description: Multi-model, multi-metric evaluation
-dataset: comprehensive_tests.jsonl
-sample_tags: [production]
-setup_script: setup.py:prepare
-
-target:
- kind: agent
- agent_script: factory.py:CustomFactory
- base_url: https://api.letta.com
- api_key: ${LETTA_API_KEY}
- project_id: proj_abc123
- model_configs: [gpt-4o-mini, claude-3-5-sonnet]
-
-graders:
- answer:
- kind: tool
- function: exact_match
- extractor: last_assistant
-
- tool_usage:
- kind: tool
- function: contains
- extractor: tool_arguments
- extractor_config:
- tool_name: search
-
- memory:
- kind: tool
- function: contains
- extractor: memory_block
- extractor_config:
- block_label: human
-
-gate:
- metric_key: answer
- metric: accuracy
- op: gte
- value: 0.9
- pass_op: gte
- pass_value: 1.0
-```
-
-## Validation
-
-Validate your suite before running:
-
-```bash
-letta-evals validate suite.yaml
-```
-
-## Next Steps
-
-- [Targets](../concepts/targets.md) - Understanding agent sources and configuration
-- [Graders](../concepts/graders.md) - Tool graders vs rubric graders
-- [Extractors](../concepts/extractors.md) - What to extract from agent responses
-- [Gates](../concepts/gates.md) - Setting pass/fail criteria
diff --git a/fern/pages/evals/configuration/suite-yaml.mdx b/fern/pages/evals/configuration/suite-yaml.mdx
deleted file mode 100644
index daf7f145..00000000
--- a/fern/pages/evals/configuration/suite-yaml.mdx
+++ /dev/null
@@ -1,427 +0,0 @@
-# Suite YAML Reference
-
-Complete reference for suite configuration files.
-
-A **suite** is a YAML file that defines an evaluation: what agent to test, what dataset to use, how to grade responses, and what criteria determine pass/fail. This is your evaluation specification.
-
-
-**Quick overview:**
-- **name**: Identifier for your evaluation
-- **dataset**: JSONL file with test cases
-- **target**: Which agent to evaluate (via file, ID, or script)
-- **graders**: How to score responses (tool or rubric graders)
-- **gate**: Pass/fail criteria
-
-
-See [Getting Started](/guides/evals/getting-started) for a tutorial, or [Core Concepts](/guides/evals/concepts/suites) for conceptual overview.
-
-## File Structure
-
-```yaml
-name: string (required)
-description: string (optional)
-dataset: path (required)
-max_samples: integer (optional)
-sample_tags: array (optional)
-num_runs: integer (optional)
-setup_script: string (optional)
-
-target: object (required)
- kind: "agent"
- base_url: string
- api_key: string
- timeout: float
- project_id: string
- agent_id: string (one of: agent_id, agent_file, agent_script)
- agent_file: path
- agent_script: string
- model_configs: array
- model_handles: array
-
-graders: object (required)
- : object
- kind: "tool" | "rubric"
- display_name: string
- extractor: string
- extractor_config: object
- # Tool grader fields
- function: string
- # Rubric grader fields (LLM API)
- prompt: string
- prompt_path: path
- model: string
- temperature: float
- provider: string
- max_retries: integer
- timeout: float
- rubric_vars: array
- # Rubric grader fields (agent-as-judge)
- agent_file: path
- judge_tool_name: string
-
-gate: object (required)
- metric_key: string
- metric: "avg_score" | "accuracy"
- op: "gte" | "gt" | "lte" | "lt" | "eq"
- value: float
- pass_op: "gte" | "gt" | "lte" | "lt" | "eq"
- pass_value: float
-```
-
-## Top-Level Fields
-
-### name (required)
-
-Suite name, used in output and results.
-
-**Type**: string
-
-```yaml
-name: question-answering-eval
-```
-
-### description (optional)
-
-Human-readable description of what the suite tests.
-
-**Type**: string
-
-```yaml
-description: Tests agent's ability to answer factual questions accurately
-```
-
-### dataset (required)
-
-Path to JSONL dataset file. Relative paths are resolved from the suite YAML location.
-
-**Type**: path (string)
-
-```yaml
-dataset: ./datasets/qa.jsonl
-dataset: /absolute/path/to/dataset.jsonl
-```
-
-### max_samples (optional)
-
-Limit the number of samples to evaluate. Useful for quick tests.
-
-**Type**: integer | **Default**: All samples
-
-```yaml
-max_samples: 10 # Only evaluate first 10 samples
-```
-
-### sample_tags (optional)
-
-Filter samples by tags. Only samples with ALL specified tags are evaluated.
-
-**Type**: array of strings
-
-```yaml
-sample_tags: [math, easy] # Only samples tagged with both
-```
-
-### num_runs (optional)
-
-Number of times to run the evaluation suite.
-
-**Type**: integer | **Default**: 1
-
-```yaml
-num_runs: 5 # Run the evaluation 5 times
-```
-
-### setup_script (optional)
-
-Path to Python script with setup function.
-
-**Type**: string (format: `path/to/script.py:function_name`)
-
-```yaml
-setup_script: setup.py:prepare_environment
-```
-
-## target (required)
-
-Configuration for the agent being evaluated.
-
-### kind (required)
-
-Type of target. Currently only `"agent"` is supported.
-
-```yaml
-target:
- kind: agent
-```
-
-### base_url (optional)
-
-Letta server URL. **Default**: `https://api.letta.com`
-
-```yaml
-target:
- base_url: https://api.letta.com
- # or
- base_url: https://api.letta.com
-```
-
-### api_key (optional)
-
-API key for Letta authentication. Can also be set via `LETTA_API_KEY` environment variable.
-
-```yaml
-target:
- api_key: your-api-key-here
-```
-
-### timeout (optional)
-
-Request timeout in seconds. **Default**: 300.0
-
-```yaml
-target:
- timeout: 600.0 # 10 minutes
-```
-
-### Agent Source (required, pick one)
-
-Exactly one of these must be specified:
-
-#### agent_id
-
-ID of existing agent on the server.
-
-```yaml
-target:
- agent_id: agent-123-abc
-```
-
-#### agent_file
-
-Path to `.af` agent file.
-
-```yaml
-target:
- agent_file: ./agents/my_agent.af
-```
-
-#### agent_script
-
-Path to Python script with agent factory.
-
-```yaml
-target:
- agent_script: factory.py:MyAgentFactory
-```
-
-See [Targets](/guides/evals/concepts/targets) for details on agent sources.
-
-### model_configs (optional)
-
-List of model configuration names to test. Cannot be used with `model_handles`.
-
-```yaml
-target:
- model_configs: [gpt-4o-mini, claude-3-5-sonnet]
-```
-
-### model_handles (optional)
-
-List of model handles for cloud deployments. Cannot be used with `model_configs`.
-
-```yaml
-target:
- model_handles: ["openai/gpt-4o-mini", "anthropic/claude-3-5-sonnet"]
-```
-
-## graders (required)
-
-One or more graders, each with a unique key.
-
-### kind (required)
-
-Grader type: `"tool"` or `"rubric"`.
-
-```yaml
-graders:
- my_metric:
- kind: tool
-```
-
-### extractor (required)
-
-Name of the extractor to use.
-
-```yaml
-graders:
- my_metric:
- extractor: last_assistant
-```
-
-### Tool Grader Fields
-
-#### function (required for tool graders)
-
-Name of the grading function.
-
-```yaml
-graders:
- accuracy:
- kind: tool
- function: exact_match
-```
-
-### Rubric Grader Fields
-
-#### prompt or prompt_path (required)
-
-Inline rubric prompt or path to rubric file.
-
-```yaml
-graders:
- quality:
- kind: rubric
- prompt: |
- Evaluate response quality from 0.0 to 1.0.
-```
-
-#### model (optional)
-
-LLM model for judging. **Default**: `gpt-4o-mini`
-
-```yaml
-graders:
- quality:
- kind: rubric
- model: gpt-4o
-```
-
-#### temperature (optional)
-
-Temperature for LLM generation. **Default**: 0.0
-
-```yaml
-graders:
- quality:
- kind: rubric
- temperature: 0.0
-```
-
-#### agent_file (agent-as-judge)
-
-Path to `.af` agent file to use as judge.
-
-```yaml
-graders:
- agent_judge:
- kind: rubric
- agent_file: judge.af
- prompt_path: rubric.txt
-```
-
-## gate (required)
-
-Pass/fail criteria for the evaluation.
-
-### metric_key (optional)
-
-Which grader to evaluate. If only one grader, this can be omitted.
-
-```yaml
-gate:
- metric_key: accuracy
-```
-
-### metric (optional)
-
-Which aggregate to compare: `avg_score` or `accuracy`. **Default**: `avg_score`
-
-```yaml
-gate:
- metric: avg_score
-```
-
-### op (required)
-
-Comparison operator: `gte`, `gt`, `lte`, `lt`, `eq`
-
-```yaml
-gate:
- op: gte # Greater than or equal
-```
-
-### value (required)
-
-Threshold value for comparison (0.0 to 1.0).
-
-```yaml
-gate:
- value: 0.8 # Require >= 0.8
-```
-
-## Complete Examples
-
-### Minimal Suite
-
-```yaml
-name: basic-eval
-dataset: dataset.jsonl
-
-target:
- kind: agent
- agent_file: agent.af
-
-graders:
- accuracy:
- kind: tool
- function: exact_match
- extractor: last_assistant
-
-gate:
- op: gte
- value: 0.8
-```
-
-### Multi-Metric Suite
-
-```yaml
-name: comprehensive-eval
-description: Tests accuracy and quality
-dataset: test_data.jsonl
-
-target:
- kind: agent
- agent_file: agent.af
-
-graders:
- accuracy:
- kind: tool
- function: contains
- extractor: last_assistant
-
- quality:
- kind: rubric
- prompt_path: rubrics/quality.txt
- model: gpt-4o-mini
- extractor: last_assistant
-
-gate:
- metric_key: accuracy
- op: gte
- value: 0.85
-```
-
-## Validation
-
-Validate your suite before running:
-
-```bash
-letta-evals validate suite.yaml
-```
-
-## Next Steps
-
-- [Targets](/guides/evals/concepts/targets) - Understanding agent sources and configuration
-- [Graders](/guides/evals/concepts/graders) - Tool graders vs rubric graders
-- [Extractors](/guides/evals/concepts/extractors) - What to extract from agent responses
-- [Gates](/guides/evals/concepts/gates) - Setting pass/fail criteria
diff --git a/fern/pages/evals/extractors/builtin.md b/fern/pages/evals/extractors/builtin.md
deleted file mode 100644
index 13caae88..00000000
--- a/fern/pages/evals/extractors/builtin.md
+++ /dev/null
@@ -1,218 +0,0 @@
-# Built-in Extractors Reference
-
-Letta Evals provides a set of built-in extractors that cover the most common extraction needs. These extractors let you pull specific content from agent conversations without writing any custom code.
-
-**What are extractors?** Extractors determine what part of an agent's response gets evaluated. They take the full conversation trajectory (all messages, tool calls, and state changes) and extract just the piece you want to grade.
-
-**Common use cases:**
-- Extract the agent's final answer (`last_assistant`)
-- Check what tools were called and with what arguments (`tool_arguments`)
-- Verify memory was updated correctly (`memory_block`)
-- Parse structured output with regex (`pattern`)
-- Get all messages from a conversation (`all_assistant`)
-
-**Quick example:**
-```yaml
-graders:
- accuracy:
- kind: tool
- function: exact_match
- extractor: last_assistant # Extract final response
-```
-
-Each extractor below can be used with any grader by specifying it in your suite YAML. For custom extraction logic, see [Custom Extractors](./custom.md).
-
-## `last_assistant`
-
-Extracts the last assistant message content.
-
-**Configuration**: None required
-
-**Example**:
-```yaml
-extractor: last_assistant
-```
-
-**Use case**: Most common - get the agent's final response
-
-**Output**: Content of the last assistant message
-
-## `first_assistant`
-
-Extracts the first assistant message content.
-
-**Configuration**: None required
-
-**Example**:
-```yaml
-extractor: first_assistant
-```
-
-**Use case**: Test immediate responses before tool usage
-
-**Output**: Content of the first assistant message
-
-## `all_assistant`
-
-Concatenates all assistant messages with a separator.
-
-**Configuration**:
-- `separator` (optional): String to join messages (default: `"\n"`)
-
-**Example**:
-```yaml
-extractor: all_assistant # Get all agent messages
-extractor_config:
- separator: "\n\n" # Separate with double newlines
-```
-
-**Use case**: Evaluate complete conversation context
-
-**Output**: All assistant messages joined by separator
-
-## last_turn
-
-Extracts all assistant messages from the last conversation turn.
-
-**Configuration**:
-- `separator` (optional): String to join messages (default: `"\n"`)
-
-**Example**:
-```yaml
-extractor: last_turn # Get messages from final turn
-extractor_config:
- separator: " " # Join with spaces
-```
-
-**Use case**: When agent makes multiple statements in final turn
-
-**Output**: Assistant messages from last turn joined by separator
-
-## pattern
-
-Extracts content matching a regex pattern.
-
-**Configuration**:
-- `pattern` (required): Regex pattern to match
-- `group` (optional): Capture group to extract (default: 0)
-- `search_all` (optional): Find all matches vs first match (default: false)
-
-**Example**:
-```yaml
-extractor: pattern # Extract using regex
-extractor_config:
- pattern: 'Result: (\d+)' # Match "Result: " followed by digits
- group: 1 # Extract just the number (capture group 1)
-```
-
-**Use case**: Extract structured content (numbers, codes, formatted output)
-
-**Output**: Matched pattern or capture group
-
-## tool_arguments
-
-Extracts arguments from a specific tool call.
-
-**Configuration**:
-- `tool_name` (required): Name of the tool to extract from
-
-**Example**:
-```yaml
-extractor: tool_arguments # Extract tool call arguments
-extractor_config:
- tool_name: search # Get arguments from "search" tool
-```
-
-**Use case**: Validate tool was called with correct arguments
-
-**Output**: JSON string of tool arguments
-
-Example output: `{"query": "pandas", "limit": 10}`
-
-## tool_output
-
-Extracts the return value from a specific tool call.
-
-**Configuration**:
-- `tool_name` (required): Name of the tool whose output to extract
-
-**Example**:
-```yaml
-extractor: tool_output # Extract tool return value
-extractor_config:
- tool_name: search # Get return value from "search" tool
-```
-
-**Use case**: Check tool return values
-
-**Output**: Tool return value as string
-
-## after_marker
-
-Extracts content after a specific marker string.
-
-**Configuration**:
-- `marker` (required): String marker to search for
-- `include_marker` (optional): Include marker in output (default: false)
-
-**Example**:
-```yaml
-extractor: after_marker # Extract content after a marker
-extractor_config:
- marker: "ANSWER:" # Find this marker in the response
- include_marker: false # Don't include "ANSWER:" in output
-```
-
-**Use case**: Extract structured responses with markers
-
-**Output**: Content after the marker
-
-Example: From "Analysis... ANSWER: Paris", extracts "Paris"
-
-## memory_block
-
-Extracts content from a specific memory block.
-
-**Configuration**:
-- `block_label` (required): Label of the memory block
-
-**Example**:
-```yaml
-extractor: memory_block # Extract from agent memory
-extractor_config:
- block_label: human # Get content from "human" memory block
-```
-
-**Use case**: Validate agent memory updates
-
-**Output**: Content of the specified memory block
-
-**Important**: This extractor requires agent_state, which adds overhead. The runner automatically fetches it when needed.
-
-## Quick Reference Table
-
-| Extractor | Config Required | Use Case | Agent State? |
-|-----------|----------------|----------|--------------|
-| `last_assistant` | No | Final response | No |
-| `first_assistant` | No | Initial response | No |
-| `all_assistant` | Optional | Full conversation | No |
-| `last_turn` | Optional | Final turn messages | No |
-| `pattern` | Yes | Regex extraction | No |
-| `tool_arguments` | Yes | Tool call args | No |
-| `tool_output` | Yes | Tool return value | No |
-| `after_marker` | Yes | Marker-based extraction | No |
-| `memory_block` | Yes | Memory content | Yes |
-
-## Listing Extractors
-
-See all available extractors:
-
-```bash
-letta-evals list-extractors
-```
-
-## Next Steps
-
-- [Custom Extractors](./custom.md) - Write your own extraction logic
-- [Core Concepts: Extractors](../concepts/extractors.md) - How extractors work in the evaluation flow
-- [Graders](../concepts/graders.md) - Using extractors with graders
diff --git a/fern/pages/evals/extractors/custom.md b/fern/pages/evals/extractors/custom.md
deleted file mode 100644
index 5edfbc7b..00000000
--- a/fern/pages/evals/extractors/custom.md
+++ /dev/null
@@ -1,409 +0,0 @@
-# Custom Extractors
-
-Create your own extractors to pull exactly what you need from agent trajectories.
-
-While built-in extractors cover common cases (last assistant message, tool arguments, memory blocks), custom extractors let you implement specialized extraction logic for your specific use case.
-
-## Why Custom Extractors?
-
-Use custom extractors when you need to:
-- **Extract structured data**: Parse JSON fields from agent responses
-- **Filter specific patterns**: Extract code blocks, URLs, or formatted content
-- **Combine data sources**: Merge information from multiple messages or memory blocks
-- **Count occurrences**: Track how many times something happened in the conversation
-- **Complex logic**: Implement domain-specific extraction that built-ins can't handle
-
-**Example**: You want to test if your agent correctly stores fruit preferences in memory using the `memory_insert` tool. A custom extractor can grab the tool call arguments, and a custom grader can verify the fruit name is in the right memory block.
-
-## Quick Example
-
-Here's a real custom extractor that pulls `memory_insert` tool call arguments:
-
-```python
-from typing import List
-from letta_client import LettaMessageUnion, ToolCallMessage
-from letta_evals.decorators import extractor
-
-@extractor
-def memory_insert_extractor(trajectory: List[List[LettaMessageUnion]], config: dict) -> str:
- """Extract memory_insert tool call arguments from trajectory."""
- for turn in trajectory:
- for message in turn:
- if isinstance(message, ToolCallMessage) and message.tool_call.name == "memory_insert":
- return message.tool_call.arguments
-
- return "{}" # Return empty JSON if not found
-```
-
-This extractor:
-1. Loops through all conversation turns
-2. Finds `ToolCallMessage` objects
-3. Checks if the tool is `memory_insert`
-4. Returns the JSON arguments
-5. Returns `"{}"` if no matching tool call found
-
-You can then pair this with a custom grader to verify the arguments are correct (see [Custom Graders](../advanced/custom-graders.md)).
-
-## Basic Structure
-
-```python
-from typing import List, Optional
-from letta_client import LettaMessageUnion, AgentState
-from letta_evals.decorators import extractor
-
-@extractor
-def my_extractor(
- trajectory: List[List[LettaMessageUnion]],
- config: dict,
- agent_state: Optional[AgentState] = None
-) -> str:
- """Your custom extraction logic."""
- # Extract and return content
- return extracted_text
-```
-
-## The @extractor Decorator
-
-The `@extractor` decorator registers your function:
-
-```python
-from letta_evals.decorators import extractor
-
-@extractor # Makes this available as "my_extractor"
-def my_extractor(trajectory, config, agent_state=None):
- ...
-```
-
-## Function Signature
-
-### Required Parameters
-
-- `trajectory`: List of conversation turns, each containing messages
-- `config`: Dictionary with extractor configuration from YAML
-
-### Optional Parameters
-
-- `agent_state`: Agent state (only needed if extracting from memory blocks or other agent state). Most extractors only need the trajectory.
-
-### Return Value
-
-Must return a string - the extracted content to be graded.
-
-## Trajectory Structure
-
-The trajectory is a list of turns:
-
-```python
-[
- # Turn 1
- [
- UserMessage(...),
- AssistantMessage(...),
- ToolCallMessage(...),
- ToolReturnMessage(...)
- ],
- # Turn 2
- [
- AssistantMessage(...)
- ]
-]
-```
-
-Message types:
-- `UserMessage`: User input
-- `AssistantMessage`: Agent response
-- `ToolCallMessage`: Tool invocation
-- `ToolReturnMessage`: Tool result
-- `SystemMessage`: System messages
-
-## Configuration
-
-Access extractor config via the `config` parameter:
-
-```yaml
-extractor: my_extractor
-extractor_config:
- max_length: 100 # Truncate output at 100 chars
- include_metadata: true # Include metadata in extraction
-```
-
-```python
-@extractor
-def my_extractor(trajectory, config, agent_state=None):
- max_length = config.get("max_length", 500)
- include_metadata = config.get("include_metadata", False)
- ...
-```
-
-## Examples
-
-### Extract Last N Messages
-
-```python
-from letta_evals.decorators import extractor
-from letta_evals.extractors.utils import get_assistant_messages, flatten_content
-
-@extractor
-def last_n_messages(trajectory, config, agent_state=None):
- """Extract the last N assistant messages."""
- n = config.get("n", 3)
- messages = get_assistant_messages(trajectory)
- last_n = messages[-n:] if len(messages) >= n else messages
- contents = [flatten_content(msg.content) for msg in last_n]
- return "\n".join(contents)
-```
-
-Usage:
-```yaml
-extractor: last_n_messages # Use custom extractor
-extractor_config:
- n: 3 # Extract last 3 assistant messages
-```
-
-### Extract JSON Field
-
-```python
-import json
-from letta_evals.decorators import extractor
-from letta_evals.extractors.utils import get_assistant_messages, flatten_content
-
-@extractor
-def json_field(trajectory, config, agent_state=None):
- """Extract a specific field from JSON response."""
- field_name = config.get("field", "result")
- messages = get_assistant_messages(trajectory)
-
- if not messages:
- return ""
-
- content = flatten_content(messages[-1].content)
-
- try:
- data = json.loads(content)
- return str(data.get(field_name, ""))
- except json.JSONDecodeError:
- return ""
-```
-
-Usage:
-```yaml
-extractor: json_field # Parse JSON from agent response
-extractor_config:
- field: result # Extract the "result" field from JSON
-```
-
-### Extract Code Blocks
-
-```python
-import re
-from letta_evals.decorators import extractor
-from letta_evals.extractors.utils import get_assistant_messages, flatten_content
-
-@extractor
-def code_blocks(trajectory, config, agent_state=None):
- """Extract all code blocks from messages."""
- language = config.get("language", None) # Optional: filter by language
- messages = get_assistant_messages(trajectory)
-
- code_pattern = r'```(?:(\w+)\n)?(.*?)```'
- all_code = []
-
- for msg in messages:
- content = flatten_content(msg.content)
- matches = re.findall(code_pattern, content, re.DOTALL)
-
- for lang, code in matches:
- if language is None or lang == language:
- all_code.append(code.strip())
-
- return "\n\n".join(all_code)
-```
-
-Usage:
-```yaml
-extractor: code_blocks # Extract code from markdown blocks
-extractor_config:
- language: python # Optional: only extract Python code blocks
-```
-
-### Extract Tool Call Count
-
-```python
-from letta_client import ToolCallMessage
-from letta_evals.decorators import extractor
-
-@extractor
-def tool_call_count(trajectory, config, agent_state=None):
- """Count how many times a specific tool was called."""
- tool_name = config.get("tool_name")
- count = 0
-
- for turn in trajectory:
- for message in turn:
- if isinstance(message, ToolCallMessage):
- if tool_name is None or message.tool_call.name == tool_name:
- count += 1
-
- return str(count)
-```
-
-Usage:
-```yaml
-extractor: tool_call_count # Count tool invocations
-extractor_config:
- tool_name: search # Optional: count only "search" tool calls
-```
-
-### Extract Multiple Memory Blocks
-
-```python
-from letta_evals.decorators import extractor
-
-@extractor
-def multiple_memory_blocks(trajectory, config, agent_state=None):
- """Extract and concatenate multiple memory blocks."""
- if agent_state is None:
- return ""
-
- block_labels = config.get("block_labels", ["human", "persona"])
- separator = config.get("separator", "\n---\n")
-
- blocks = []
- for block in agent_state.memory.blocks:
- if block.label in block_labels:
- blocks.append(f"{block.label}: {block.value}")
-
- return separator.join(blocks)
-```
-
-Usage:
-```yaml
-extractor: multiple_memory_blocks # Combine multiple memory blocks
-extractor_config:
- block_labels: [human, persona] # Which blocks to extract
- separator: "\n---\n" # How to separate them in output
-```
-
-## Helper Utilities
-
-The framework provides helper functions:
-
-### get_assistant_messages
-
-```python
-from letta_evals.extractors.utils import get_assistant_messages
-
-messages = get_assistant_messages(trajectory)
-# Returns list of AssistantMessage objects
-```
-
-### get_last_turn_messages
-
-```python
-from letta_evals.extractors.utils import get_last_turn_messages
-from letta_client import AssistantMessage
-
-messages = get_last_turn_messages(trajectory, AssistantMessage)
-# Returns assistant messages from last turn
-```
-
-### flatten_content
-
-```python
-from letta_evals.extractors.utils import flatten_content
-
-text = flatten_content(message.content)
-# Converts complex content to plain text
-```
-
-## Agent State Requirements
-
-If your extractor needs agent state, include it in the signature:
-
-```python
-@extractor
-def my_extractor(trajectory, config, agent_state: Optional[AgentState] = None):
- if agent_state is None:
- raise RuntimeError("This extractor requires agent_state")
-
- # Use agent_state.memory.blocks, etc.
- ...
-```
-
-The runner will automatically fetch agent state when your extractor is used.
-
-**Note**: Fetching agent state adds overhead. Only use when necessary.
-
-## Using Custom Extractors
-
-### Method 1: Custom Evaluators File
-
-Create `custom_evaluators.py`:
-
-```python
-from letta_evals.decorators import extractor
-
-@extractor
-def my_extractor(trajectory, config, agent_state=None):
- ...
-```
-
-The file will be discovered automatically if in the same directory.
-
-### Method 2: Setup Script
-
-Use a setup script to import custom extractors before the suite runs:
-
-```python
-# setup.py
-from letta_evals.models import SuiteSpec
-import custom_extractors # Imports and registers your @extractor functions
-
-def prepare_environment(suite: SuiteSpec) -> None:
- # Runs before evaluation starts
- pass
-```
-
-```yaml
-setup_script: setup.py:prepare_environment # Import custom extractors
-
-graders:
- my_metric:
- extractor: my_extractor # Now available from custom_extractors
-```
-
-## Testing Your Extractor
-
-```python
-from letta_client import AssistantMessage
-
-# Mock trajectory
-trajectory = [
- [
- AssistantMessage(
- content="The answer is 42",
- role="assistant"
- )
- ]
-]
-
-config = {"max_length": 100}
-result = my_extractor(trajectory, config)
-print(f"Extracted: {result}")
-```
-
-## Best Practices
-
-1. **Handle empty trajectories**: Check if messages exist
-2. **Return strings**: Always return a string, not None
-3. **Use config for flexibility**: Make behavior configurable
-4. **Document required config**: Explain config parameters
-5. **Handle errors gracefully**: Return empty string on error
-6. **Keep it fast**: Extractors run for every sample
-7. **Use helper utilities**: Leverage built-in functions
-
-## Next Steps
-
-- [Built-in Extractors](./builtin.md) - Learn from examples
-- [Custom Graders](../advanced/custom-graders.md) - Pair with custom grading
-- [Core Concepts](../concepts/extractors.md) - How extractors work
diff --git a/fern/pages/evals/getting-started.md b/fern/pages/evals/getting-started.md
deleted file mode 100644
index 9520037d..00000000
--- a/fern/pages/evals/getting-started.md
+++ /dev/null
@@ -1,325 +0,0 @@
-# Getting Started with Letta Evals
-
-This guide will help you get up and running with Letta Evals in minutes.
-
-## What is Letta Evals?
-
-Letta Evals is a framework for testing Letta AI agents. It allows you to:
-
-- Test agent responses against expected outputs
-- Evaluate subjective quality using LLM judges
-- Test tool usage and memory updates
-- Track metrics across multiple evaluation runs
-- Gate deployments on quality thresholds
-
-Unlike most evaluation frameworks designed for simple input-output models, Letta Evals is built for [stateful agents](https://www.letta.com/blog/stateful-agents) that maintain memory, use tools, and evolve over time.
-
-## Prerequisites
-
-- Python 3.11 or higher
-- A running Letta server ([local](https://docs.letta.com/guides/selfhosting) or [Letta Cloud](https://docs.letta.com/guides/cloud/overview))
-- A Letta agent to test, either in agent file format or by ID (see [Targets](./concepts/targets.md) for more details)
-
-## Installation
-
-```bash
-pip install letta-evals
-```
-
-Or with uv:
-
-```bash
-uv pip install letta-evals
-```
-
-## Getting an Agent to Test
-
-Before you can run evaluations, you need a Letta agent. You have two options:
-
-### Option 1: Use an Agent File (.af)
-
-Export an existing agent to a file using the Letta SDK:
-
-```python
-from letta_client import Letta
-import os
-
-client = Letta(
- base_url="http://localhost:8283", # or https://api.letta.com for Letta Cloud
- token=os.getenv("LETTA_API_KEY") # required for Letta Cloud
-)
-
-# Export an agent to a file
-agent_file = client.agents.export_file(agent_id="agent-123")
-
-# Save to disk
-with open("my_agent.af", "w") as f:
- f.write(agent_file)
-```
-
-Or export via the Agent Development Environment (ADE) by selecting "Export Agent".
-
-This creates an `.af` file which you can reference in your suite configuration:
-
-```yaml
-target:
- kind: agent
- agent_file: my_agent.af
-```
-
-**How it works:** When using an agent file, a fresh agent instance is created for each sample in your dataset. Each test runs independently with a clean slate, making this ideal for parallel testing across different inputs.
-
-**Example:** If your dataset has 5 samples, 5 separate agents will be created and can run in parallel. Each agent starts fresh with no memory of the other tests.
-
-### Option 2: Use an Existing Agent ID
-
-If you already have a running agent, use its ID directly:
-
-```python
-from letta_client import Letta
-import os
-
-client = Letta(
- base_url="http://localhost:8283", # or https://api.letta.com for Letta Cloud
- token=os.getenv("LETTA_API_KEY") # required for Letta Cloud
-)
-
-# List all agents
-agents = client.agents.list()
-for agent in agents:
- print(f"Agent: {agent.name}, ID: {agent.id}")
-```
-
-Then reference it in your suite:
-
-```yaml
-target:
- kind: agent
- agent_id: agent-abc-123
-```
-
-**How it works:** The same agent instance is used for all samples, processing them sequentially. The agent's state (memory, message history) carries over between samples, making the dataset behave more like a conversation script than independent test cases.
-
-**Example:** If your dataset has 5 samples, they all run against the same agent one after another. The agent "remembers" each previous interaction, so sample 3 can reference information from samples 1 and 2.
-
-### Which Should You Use?
-
-**Agent File (.af)** - Use when testing independent scenarios
-
-Best for testing how the agent responds to independent, isolated inputs. Each sample gets a fresh agent with no prior context. Tests can run in parallel.
-
-**Typical scenarios:**
-- "How does the agent answer different questions?"
-- "Does the agent correctly use tools for various tasks?"
-- "Testing behavior across different prompts"
-
-**Agent ID** - Use when testing conversational flows
-
-Best for testing conversational flows or scenarios where context should build up over time. The agent's state accumulates as it processes each sample sequentially.
-
-**Typical scenarios:**
-- "Does the agent remember information across a conversation?"
-- "How does the agent's memory evolve over multiple exchanges?"
-- "Simulating a realistic user session with multiple requests"
-
-**Recommendation:** For most evaluation scenarios, use agent files to ensure consistent, reproducible test conditions. Only use agent IDs when you specifically want to test stateful, sequential interactions.
-
-For more details on agent lifecycle and testing behaviors, see the [Targets guide](./concepts/targets.md#agent-lifecycle-and-testing-behavior).
-
-## Quick Start
-
-Let's create your first evaluation in 3 steps:
-
-### 1. Create a Test Dataset
-
-Create a file named `dataset.jsonl`:
-
-```jsonl
-{"input": "What's the capital of France?", "ground_truth": "Paris"}
-{"input": "Calculate 2+2", "ground_truth": "4"}
-{"input": "What color is the sky?", "ground_truth": "blue"}
-```
-
-Each line is a JSON object with:
-- `input`: The prompt to send to your agent
-- `ground_truth`: The expected answer (used for grading)
-
-Note: `ground_truth` is optional for some graders (like rubric graders), but required for tool graders like `contains` and `exact_match`.
-
-Read more about [Datasets](./concepts/datasets.md) for details on how to create your dataset.
-
-### 2. Create a Suite Configuration
-
-Create a file named `suite.yaml`:
-
-```yaml
-name: my-first-eval
-dataset: dataset.jsonl
-
-target:
- kind: agent
- agent_file: my_agent.af # Path to your agent file
- base_url: http://localhost:8283 # Your Letta server
-
-graders:
- quality:
- kind: tool
- function: contains # Check if response contains the ground truth
- extractor: last_assistant # Use the last assistant message
-
-gate:
- metric_key: quality
- op: gte
- value: 0.75 # Require 75% pass rate
-```
-
-The suite configuration defines:
-- The [dataset](./concepts/datasets.md) to use
-- The [agent](./concepts/targets.md) to test
-- The [graders](./concepts/graders.md) to use
-- The [gate](./concepts/gates.md) criteria
-
-Read more about [Suites](./concepts/suites.md) for details on how to configure your evaluation.
-
-### 3. Run the Evaluation
-
-Run your evaluation with the following command:
-
-```bash
-letta-evals run suite.yaml
-```
-
-You'll see real-time progress as your evaluation runs:
-
-```
-Running evaluation: my-first-eval
-━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3/3 100%
-✓ PASSED (2.25/3.00 avg, 75.0% pass rate)
-```
-
-Read more about [CLI Commands](./cli/commands.md) for details about the available commands and options.
-
-## Understanding the Results
-
-The core evaluation flow is:
-
-**Dataset → Target (Agent) → Extractor → Grader → Gate → Result**
-
-The evaluation runner:
-1. Loads your dataset
-2. Sends each input to your agent (Target)
-3. Extracts the relevant information (using the Extractor)
-4. Grades the response (using the Grader function)
-5. Computes aggregate metrics
-6. Checks if metrics pass the Gate criteria
-
-The output shows:
-- **Average score**: Mean score across all samples
-- **Pass rate**: Percentage of samples that passed
-- **Gate status**: Whether the evaluation passed or failed overall
-
-## Next Steps
-
-Now that you've run your first evaluation, explore more advanced features:
-
-- [Core Concepts](./concepts/overview.md) - Understand suites, datasets, graders, and extractors
-- [Grader Types](./concepts/graders.md) - Learn about tool graders vs rubric graders
-- [Multi-Metric Evaluation](./graders/multi-metric.md) - Test multiple aspects simultaneously
-- [Custom Graders](./advanced/custom-graders.md) - Write custom grading functions
-- [Multi-Turn Conversations](./advanced/multi-turn-conversations.md) - Test conversational memory
-
-## Common Use Cases
-
-### Strict Answer Checking
-
-Use exact matching for cases where the answer must be precisely correct:
-
-```yaml
-graders:
- accuracy:
- kind: tool
- function: exact_match
- extractor: last_assistant
-```
-
-### Subjective Quality Evaluation
-
-Use an LLM judge to evaluate subjective qualities like helpfulness or tone:
-
-```yaml
-graders:
- quality:
- kind: rubric
- prompt_path: rubric.txt
- model: gpt-4o-mini
- extractor: last_assistant
-```
-
-Then create `rubric.txt`:
-```
-Rate the helpfulness and accuracy of the response.
-- Score 1.0 if helpful and accurate
-- Score 0.5 if partially helpful
-- Score 0.0 if unhelpful or wrong
-```
-
-### Testing Tool Calls
-
-Verify that your agent calls specific tools with expected arguments:
-
-```yaml
-graders:
- tool_check:
- kind: tool
- function: contains
- extractor: tool_arguments
- extractor_config:
- tool_name: search
-```
-
-### Testing Memory Persistence
-
-Check if the agent correctly updates its memory blocks:
-
-```yaml
-graders:
- memory_check:
- kind: tool
- function: contains
- extractor: memory_block
- extractor_config:
- block_label: human
-```
-
-## Troubleshooting
-
-**"Agent file not found"**
-
-Make sure your `agent_file` path is correct. Paths are relative to the suite YAML file location. Use absolute paths if needed:
-
-```yaml
-target:
- agent_file: /absolute/path/to/my_agent.af
-```
-
-**"Connection refused"**
-
-Your Letta server isn't running or isn't accessible. Start it with:
-
-```bash
-letta server
-```
-
-By default, it runs at `http://localhost:8283`.
-
-**"No ground_truth provided"**
-
-Tool graders like `exact_match` and `contains` require `ground_truth` in your dataset. Either:
-- Add `ground_truth` to your samples, or
-- Use a rubric grader which doesn't require ground truth
-
-**Agent didn't respond as expected**
-
-Try testing your agent manually first using the Letta SDK or Agent Development Environment (ADE) to see how it behaves before running evaluations. See the [Letta documentation](https://docs.letta.com) for more information.
-
-For more help, see the [Troubleshooting Guide](./troubleshooting.md).
diff --git a/fern/pages/evals/getting-started.mdx b/fern/pages/evals/getting-started.mdx
deleted file mode 100644
index fc5c99e3..00000000
--- a/fern/pages/evals/getting-started.mdx
+++ /dev/null
@@ -1,263 +0,0 @@
-# Getting Started
-
-Run your first Letta agent evaluation in 5 minutes.
-
-## Prerequisites
-
-- Python 3.11 or higher
-- A running Letta server (local or Letta Cloud)
-- A Letta agent to test, either in agent file format or by ID (see [Targets](/guides/evals/concepts/targets) for more details)
-
-## Installation
-
-```bash
-pip install letta-evals
-```
-
-Or with uv:
-
-```bash
-uv pip install letta-evals
-```
-
-## Getting an Agent to Test
-
-Export an existing agent to a file using the Letta SDK:
-
-```python
-from letta_client import Letta
-import os
-
-# Connect to Letta Cloud
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# Export an agent to a file
-agent_file = client.agents.export_file(agent_id="agent-123")
-
-# Save to disk
-with open("my_agent.af", "w") as f:
- f.write(agent_file)
-```
-
-Or export via the Agent Development Environment (ADE) by selecting "Export Agent".
-
-Then reference it in your suite:
-
-```yaml
-target:
- kind: agent
- agent_file: my_agent.af
-```
-
-
-**Other options:** You can also use existing agents by ID or programmatically generate agents. See [Targets](/guides/evals/concepts/targets) for all agent configuration options.
-
-
-## Quick Start
-
-Let's create your first evaluation in 3 steps:
-
-### 1. Create a Test Dataset
-
-Create a file named `dataset.jsonl`:
-
-```jsonl
-{"input": "What's the capital of France?", "ground_truth": "Paris"}
-{"input": "Calculate 2+2", "ground_truth": "4"}
-{"input": "What color is the sky?", "ground_truth": "blue"}
-```
-
-Each line is a JSON object with:
-- `input`: The prompt to send to your agent
-- `ground_truth`: The expected answer (used for grading)
-
-
-`ground_truth` is optional for some graders (like rubric graders), but required for tool graders like `contains` and `exact_match`.
-
-
-Read more about [Datasets](/guides/evals/concepts/datasets) for details on how to create your dataset.
-
-### 2. Create a Suite Configuration
-
-Create a file named `suite.yaml`:
-
-```yaml
-name: my-first-eval
-dataset: dataset.jsonl
-
-target:
- kind: agent
- agent_file: my_agent.af # Path to your agent file
- base_url: https://api.letta.com # Letta Cloud (default)
- token: ${LETTA_API_KEY} # Your API key
-
-graders:
- quality:
- kind: tool
- function: contains # Check if response contains the ground truth
- extractor: last_assistant # Use the last assistant message
-
-gate:
- metric_key: quality
- op: gte
- value: 0.75 # Require 75% pass rate
-```
-
-The suite configuration defines:
-- The [dataset](/guides/evals/concepts/datasets) to use
-- The [agent](/guides/evals/concepts/targets) to test
-- The [graders](/guides/evals/concepts/graders) to use
-- The [gate](/guides/evals/concepts/gates) criteria
-
-Read more about [Suites](/guides/evals/concepts/suites) for details on how to configure your evaluation.
-
-### 3. Run the Evaluation
-
-Run your evaluation with the following command:
-
-```bash
-letta-evals run suite.yaml
-```
-
-You'll see real-time progress as your evaluation runs:
-
-```
-Running evaluation: my-first-eval
-━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3/3 100%
-✓ PASSED (2.25/3.00 avg, 75.0% pass rate)
-```
-
-Read more about [CLI Commands](/guides/evals/cli/commands) for details about the available commands and options.
-
-## Understanding the Results
-
-The core evaluation flow is:
-
-**Dataset → Target (Agent) → Extractor → Grader → Gate → Result**
-
-The evaluation runner:
-1. Loads your dataset
-2. Sends each input to your agent (Target)
-3. Extracts the relevant information (using the Extractor)
-4. Grades the response (using the Grader function)
-5. Computes aggregate metrics
-6. Checks if metrics pass the Gate criteria
-
-The output shows:
-- **Average score**: Mean score across all samples
-- **Pass rate**: Percentage of samples that passed
-- **Gate status**: Whether the evaluation passed or failed overall
-
-## Next Steps
-
-Now that you've run your first evaluation, explore more advanced features:
-
-- [Core Concepts](/guides/evals/concepts/overview) - Understand suites, datasets, graders, and extractors
-- [Grader Types](/guides/evals/concepts/graders) - Learn about tool graders vs rubric graders
-- [Multi-Metric Evaluation](/guides/guides/evals/graders/multi-metric) - Test multiple aspects simultaneously
-- [Custom Graders](/guides/evals/advanced/custom-graders) - Write custom grading functions
-- [Multi-Turn Conversations](/guides/evals/advanced/multi-turn-conversations) - Test conversational memory
-
-## Common Use Cases
-
-### Strict Answer Checking
-
-Use exact matching for cases where the answer must be precisely correct:
-
-```yaml
-graders:
- accuracy:
- kind: tool
- function: exact_match
- extractor: last_assistant
-```
-
-### Subjective Quality Evaluation
-
-Use an LLM judge to evaluate subjective qualities like helpfulness or tone:
-
-```yaml
-graders:
- quality:
- kind: rubric
- prompt_path: rubric.txt
- model: gpt-4o-mini
- extractor: last_assistant
-```
-
-Then create `rubric.txt`:
-```
-Rate the helpfulness and accuracy of the response.
-- Score 1.0 if helpful and accurate
-- Score 0.5 if partially helpful
-- Score 0.0 if unhelpful or wrong
-```
-
-### Testing Tool Calls
-
-Verify that your agent calls specific tools with expected arguments:
-
-```yaml
-graders:
- tool_check:
- kind: tool
- function: contains
- extractor: tool_arguments
- extractor_config:
- tool_name: search
-```
-
-### Testing Memory Persistence
-
-Check if the agent correctly updates its memory blocks:
-
-```yaml
-graders:
- memory_check:
- kind: tool
- function: contains
- extractor: memory_block
- extractor_config:
- block_label: human
-```
-
-## Troubleshooting
-
-
-**"Agent file not found"**
-
-Make sure your `agent_file` path is correct. Paths are relative to the suite YAML file location. Use absolute paths if needed:
-
-```yaml
-target:
- agent_file: /absolute/path/to/my_agent.af
-```
-
-
-
-**"Connection refused"**
-
-Your Letta server isn't running or isn't accessible. Start it using Docker:
-
-```bash
-docker run -p 8283:8283 -e OPENAI_API_KEY="your_api_key" letta/letta:latest
-```
-
-By default, it runs at `http://localhost:8283`. See the [self-hosting guide](/guides/selfhosting) for more information.
-
-
-
-**"No ground_truth provided"**
-
-Tool graders like `exact_match` and `contains` require `ground_truth` in your dataset. Either:
-- Add `ground_truth` to your samples, or
-- Use a rubric grader which doesn't require ground truth
-
-
-
-**Agent didn't respond as expected**
-
-Try testing your agent manually first using the Letta SDK or Agent Development Environment (ADE) to see how it behaves before running evaluations. See the [Letta documentation](https://docs.letta.com) for more information.
-
-
-For more help, see the [Troubleshooting Guide](/guides/evals/troubleshooting).
diff --git a/fern/pages/evals/graders/multi-metric.md b/fern/pages/evals/graders/multi-metric.md
deleted file mode 100644
index ae6f5776..00000000
--- a/fern/pages/evals/graders/multi-metric.md
+++ /dev/null
@@ -1,427 +0,0 @@
-# Multi-Metric Evaluation
-
-Evaluate multiple aspects of agent performance simultaneously in a single evaluation suite.
-
-Multi-metric evaluation allows you to define multiple graders, each measuring a different dimension of your agent's behavior. This is essential for comprehensive testing because agent quality isn't just about correctness—you also care about explanation quality, tool usage, format compliance, and more.
-
-**Example**: You might want to check that an agent gives the correct answer (tool grader with `exact_match`), explains it well (rubric grader for clarity), and calls the right tools (tool grader on `tool_arguments`). Instead of running three separate evaluations, you can test all three aspects in one run.
-
-## Why Multiple Metrics?
-
-Agents are complex systems. You might want to evaluate:
-- **Correctness**: Does the answer match the expected output?
-- **Quality**: Is the explanation clear, complete, and well-structured?
-- **Tool usage**: Does the agent call the right tools with correct arguments?
-- **Memory**: Does the agent correctly update its memory blocks?
-- **Format**: Does the output follow required formatting rules?
-
-Multi-metric evaluation lets you track all of these simultaneously, giving you a holistic view of agent performance.
-
-## Configuration
-
-Define multiple graders under the `graders` section:
-
-```yaml
-graders:
- accuracy:
- kind: tool
- function: exact_match
- extractor: last_assistant # Check if answer is exactly correct
-
- completeness:
- kind: rubric
- prompt_path: completeness.txt
- model: gpt-4o-mini
- extractor: last_assistant # LLM judge evaluates how complete the answer is
-
- tool_usage:
- kind: tool
- function: contains
- extractor: tool_arguments # Check if agent called the right tool
- extractor_config:
- tool_name: search
-```
-
-Each grader:
-- Has a unique key (e.g., `accuracy`, `completeness`)
-- Can use different kinds (tool vs rubric)
-- Can use different extractors
-- Produces independent scores
-
-## Gating on One Metric
-
-While you evaluate multiple metrics, you can only gate on one:
-
-```yaml
-graders:
- accuracy:
- kind: tool
- function: exact_match
- extractor: last_assistant # Check correctness
-
- quality:
- kind: rubric
- prompt_path: quality.txt
- model: gpt-4o-mini
- extractor: last_assistant # Evaluate subjective quality
-
-gate:
- metric_key: accuracy # Pass/fail based on accuracy only
- op: gte
- value: 0.8 # Require 80% accuracy to pass
-```
-
-The evaluation passes/fails based on `accuracy`, but results include both metrics.
-
-## Results Structure
-
-With multiple metrics, results include:
-
-### Per-Sample Results
-
-Each sample has scores for all metrics:
-
-```json
-{
- "sample": {...},
- "grades": {
- "accuracy": {"score": 1.0, "rationale": "Exact match: true"},
- "quality": {"score": 0.85, "rationale": "Good response, minor improvements possible"}
- },
- "submissions": {
- "accuracy": "Paris",
- "quality": "Paris"
- }
-}
-```
-
-Note: If all graders use the same extractor, `submission` and `grade` are also provided for backwards compatibility.
-
-### Aggregate Metrics
-
-```json
-{
- "metrics": {
- "by_metric": {
- "accuracy": {
- "avg_score_attempted": 0.95,
- "pass_rate": 95.0,
- "passed_attempts": 19,
- "failed_attempts": 1
- },
- "quality": {
- "avg_score_attempted": 0.82,
- "pass_rate": 80.0,
- "passed_attempts": 16,
- "failed_attempts": 4
- }
- }
- }
-}
-```
-
-## Use Cases
-
-### Accuracy + Quality
-
-```yaml
-graders:
- accuracy:
- kind: tool
- function: contains
- extractor: last_assistant # Does response contain the answer?
-
- quality:
- kind: rubric
- prompt_path: quality.txt
- model: gpt-4o-mini
- extractor: last_assistant # How well is it explained?
-
-gate:
- metric_key: accuracy # Must be correct to pass
- op: gte
- value: 0.9 # 90% must have correct answer
-```
-
-Gate on accuracy (must be correct), but also track quality for insights.
-
-### Content + Format
-
-```yaml
-graders:
- content:
- kind: rubric
- prompt_path: content.txt
- model: gpt-4o-mini
- extractor: last_assistant # Evaluate content quality
-
- format:
- kind: tool
- function: ascii_printable_only
- extractor: last_assistant # Check format compliance
-
-gate:
- metric_key: content # Gate on content quality
- op: gte
- value: 0.7 # Content must score 70% or higher
-```
-
-Ensure content quality while checking format constraints.
-
-### Answer + Tool Usage + Memory
-
-```yaml
-graders:
- answer:
- kind: tool
- function: contains
- extractor: last_assistant # Did the agent answer correctly?
-
- used_tools:
- kind: tool
- function: contains
- extractor: tool_arguments # Did it call the search tool?
- extractor_config:
- tool_name: search
-
- memory_updated:
- kind: tool
- function: contains
- extractor: memory_block # Did it update human memory?
- extractor_config:
- block_label: human
-
-gate:
- metric_key: answer # Gate on correctness
- op: gte
- value: 0.8 # 80% of answers must be correct
-```
-
-Comprehensive evaluation of agent behavior.
-
-### Multiple Quality Dimensions
-
-```yaml
-graders:
- accuracy:
- kind: rubric
- prompt: "Rate factual accuracy from 0.0 to 1.0"
- model: gpt-4o-mini
- extractor: last_assistant
-
- clarity:
- kind: rubric
- prompt: "Rate clarity of explanation from 0.0 to 1.0"
- model: gpt-4o-mini
- extractor: last_assistant
-
- conciseness:
- kind: rubric
- prompt: "Rate conciseness (not too verbose) from 0.0 to 1.0"
- model: gpt-4o-mini
- extractor: last_assistant
-
-gate:
- metric_key: accuracy
- op: gte
- value: 0.8
-```
-
-Track multiple subjective dimensions.
-
-## Display Names
-
-Add human-friendly names for metrics:
-
-```yaml
-graders:
- acc:
- display_name: "Accuracy"
- kind: tool
- function: exact_match
- extractor: last_assistant
-
- qual:
- display_name: "Response Quality"
- kind: rubric
- prompt_path: quality.txt
- model: gpt-4o-mini
- extractor: last_assistant
-```
-
-Display names appear in CLI output and visualizations.
-
-## Independent Extraction
-
-Each grader can extract different content:
-
-```yaml
-graders:
- final_answer:
- kind: tool
- function: contains
- extractor: last_assistant # Last thing said
-
- tool_calls:
- kind: tool
- function: contains
- extractor: all_assistant # Everything said
-
- search_usage:
- kind: tool
- function: contains
- extractor: tool_arguments # Tool arguments
- extractor_config:
- tool_name: search
-```
-
-## Analyzing Results
-
-### View All Metrics
-
-CLI output shows all metrics:
-
-```
-Results by metric:
- accuracy - Avg: 0.95, Pass: 95.0%
- quality - Avg: 0.82, Pass: 80.0%
- tool_usage - Avg: 0.88, Pass: 88.0%
-
-Gate (accuracy >= 0.9): PASSED
-```
-
-### JSON Output
-
-```bash
-letta-evals run suite.yaml --output results/
-```
-
-Produces:
-- `results/summary.json`: Aggregate metrics
-- `results/results.jsonl`: Per-sample results with all grades
-
-### Filtering Results
-
-Post-process to find patterns:
-
-```python
-import json
-
-# Load results
-with open("results/results.jsonl") as f:
- results = [json.loads(line) for line in f]
-
-# Find samples where accuracy=1.0 but quality<0.5
-issues = [
- r for r in results
- if r["grades"]["accuracy"]["score"] == 1.0
- and r["grades"]["quality"]["score"] < 0.5
-]
-
-print(f"Found {len(issues)} samples with correct but low-quality responses")
-```
-
-## Best Practices
-
-### 1. Start with Core Metric
-
-Focus on one primary metric for gating:
-
-```yaml
-gate:
- metric_key: accuracy # Most important
- op: gte
- value: 0.9
-```
-
-Use others for diagnostics.
-
-### 2. Combine Tool and Rubric
-
-Use fast tool graders for objective checks, rubric graders for quality:
-
-```yaml
-graders:
- correct:
- kind: tool # Fast, cheap
- function: contains
- extractor: last_assistant
-
- quality:
- kind: rubric # Slower, more nuanced
- prompt_path: quality.txt
- model: gpt-4o-mini
- extractor: last_assistant
-```
-
-### 3. Track Tool Usage
-
-Add a metric for expected tool calls:
-
-```yaml
-graders:
- used_search:
- kind: tool
- function: contains
- extractor: tool_arguments
- extractor_config:
- tool_name: search
-```
-
-### 4. Validate Format
-
-Include format checks alongside content:
-
-```yaml
-graders:
- content:
- kind: rubric
- prompt_path: content.txt
- model: gpt-4o-mini
- extractor: last_assistant
-
- ascii_only:
- kind: tool
- function: ascii_printable_only
- extractor: last_assistant
-```
-
-### 5. Use Display Names
-
-Make CLI output readable:
-
-```yaml
-graders:
- acc:
- display_name: "Answer Accuracy"
- kind: tool
- function: exact_match
- extractor: last_assistant
-```
-
-## Cost Implications
-
-Multiple rubric graders multiply API costs:
-
-- 1 grader: $0.00015/sample
-- 3 graders: $0.00045/sample
-- 5 graders: $0.00075/sample
-
-For 1000 samples with 3 rubric graders: ~$0.45
-
-Mix tool and rubric graders to balance cost and insight.
-
-## Performance
-
-Multiple graders run sequentially per sample, but samples run concurrently:
-
-- 1 grader: ~1s per sample
-- 3 graders (2 rubric): ~2s per sample
-
-With 10 concurrent: 1000 samples in ~3-5 minutes
-
-## Next Steps
-
-- [Tool Graders](./tool-graders.md)
-- [Rubric Graders](./rubric-graders.md)
-- [Understanding Results](../results/overview.md)
diff --git a/fern/pages/evals/graders/rubric-graders.md b/fern/pages/evals/graders/rubric-graders.md
deleted file mode 100644
index 2fa982bc..00000000
--- a/fern/pages/evals/graders/rubric-graders.md
+++ /dev/null
@@ -1,680 +0,0 @@
-# Rubric Graders
-
-Rubric graders, also called "LLM-as-judge" graders, use language models evaluate submissions based on custom criteria. They're ideal for subjective, nuanced evaluation.
-
-Rubric graders work by providing the LLM with a prompt that describes the evaluation criteria, then the language model generates a structured JSON response with a score and rationale:
-
-```json
-{
- "score": 0.85,
- "rationale": "The response is accurate and well-explained, but could be more concise."
-}
-```
-
-**Schema requirements:**
-- `score` (required): Decimal number between 0.0 and 1.0
-- `rationale` (required): String explanation of the grading decision
-
-> **Note**: OpenAI provides the best support for structured generation. Other providers may have varying quality of structured output adherence.
-
-## Overview
-
-Rubric graders:
-- Use an LLM to evaluate responses
-- Support custom evaluation criteria (rubrics)
-- Can handle subjective quality assessment
-- Return scores with explanations
-- Use JSON structured generation for reliability
-
-## Basic Configuration
-
-```yaml
-graders:
- quality:
- kind: rubric
- prompt_path: rubric.txt # Path to rubric file
- model: gpt-4o-mini # LLM model
- extractor: last_assistant
-```
-
-## Rubric Prompts
-
-Your rubric file defines the evaluation criteria. It can include placeholders:
-
-- `{input}`: The original input from the dataset
-- `{submission}`: The extracted agent response
-- `{ground_truth}`: Ground truth from dataset (if available)
-
-### Example Rubric
-
-`quality_rubric.txt`:
-```
-Evaluate the response for accuracy, completeness, and clarity.
-
-Input: {input}
-Expected answer: {ground_truth}
-Agent response: {submission}
-
-Scoring criteria:
-- 1.0: Perfect - accurate, complete, and clear
-- 0.8-0.9: Excellent - minor improvements possible
-- 0.6-0.7: Good - some gaps or unclear parts
-- 0.4-0.5: Adequate - significant issues
-- 0.2-0.3: Poor - major problems
-- 0.0-0.1: Failed - incorrect or nonsensical
-
-Provide a score between 0.0 and 1.0.
-```
-
-### Inline Prompts
-
-You can include the prompt directly in the YAML:
-
-```yaml
-graders:
- creativity:
- kind: rubric
- prompt: |
- Evaluate the creativity and originality of the response.
-
- Response: {submission}
-
- Score from 0.0 (generic) to 1.0 (highly creative).
- model: gpt-4o-mini
- extractor: last_assistant
-```
-
-## Configuration Options
-
-### prompt_path vs prompt
-
-Use exactly one:
-
-```yaml
-# Option 1: External file
-graders:
- quality:
- kind: rubric
- prompt_path: rubrics/quality.txt # Relative to suite YAML
- model: gpt-4o-mini
- extractor: last_assistant
-```
-
-```yaml
-# Option 2: Inline
-graders:
- quality:
- kind: rubric
- prompt: "Evaluate the response quality from 0.0 to 1.0"
- model: gpt-4o-mini
- extractor: last_assistant
-```
-
-### model
-
-LLM model to use for judging:
-
-```yaml
-graders:
- quality:
- kind: rubric
- prompt_path: rubric.txt
- model: gpt-4o-mini # Or gpt-4o, claude-3-5-sonnet, etc.
- extractor: last_assistant
-```
-
-Supported: Any OpenAI-compatible model
-
-**Special handling**: For reasoning models (o1, o3, gpt-5), temperature is automatically set to 1.0 even if you specify 0.0.
-
-### temperature
-
-Controls randomness in LLM generation:
-
-```yaml
-graders:
- quality:
- kind: rubric
- prompt_path: rubric.txt
- model: gpt-4o-mini
- temperature: 0.0 # Deterministic (default)
- extractor: last_assistant
-```
-
-Range: 0.0 (deterministic) to 2.0 (very random)
-
-Default: 0.0 (recommended for evaluations)
-
-### provider
-
-LLM provider:
-
-```yaml
-graders:
- quality:
- kind: rubric
- prompt_path: rubric.txt
- model: gpt-4o-mini
- provider: openai # Default
- extractor: last_assistant
-```
-
-Currently supported: `openai` (default)
-
-### max_retries
-
-Number of retry attempts if API call fails:
-
-```yaml
-graders:
- quality:
- kind: rubric
- prompt_path: rubric.txt
- model: gpt-4o-mini
- max_retries: 5 # Default
- extractor: last_assistant
-```
-
-### timeout
-
-Timeout for API calls in seconds:
-
-```yaml
-graders:
- quality:
- kind: rubric
- prompt_path: rubric.txt
- model: gpt-4o-mini
- timeout: 120.0 # Default: 2 minutes
- extractor: last_assistant
-```
-
-## How It Works
-
-1. **Prompt Building**: The rubric prompt is populated with placeholders (`{input}`, `{submission}`, `{ground_truth}`)
-2. **System Prompt**: Instructs the LLM to return JSON with `score` and `rationale` fields
-3. **Structured Output**: Uses JSON mode (`response_format: json_object`) to enforce the schema
-4. **Validation**: Extracts and validates score (clamped to 0.0-1.0) and rationale
-5. **Error Handling**: Returns score 0.0 with error message if grading fails
-
-### System Prompt
-
-The rubric grader automatically includes this system prompt:
-
-```
-You are an evaluation judge. You will be given:
-1. A rubric describing evaluation criteria
-2. An input/question
-3. A submission to evaluate
-
-Evaluate the submission according to the rubric and return a JSON response with:
-{
- "score": (REQUIRED: a decimal number between 0.0 and 1.0 inclusive),
- "rationale": "explanation of your grading decision"
-}
-
-IMPORTANT:
-- The score MUST be a number between 0.0 and 1.0 (inclusive)
-- 0.0 means complete failure, 1.0 means perfect
-- Use decimal values for partial credit (e.g., 0.25, 0.5, 0.75)
-- Be objective and follow the rubric strictly
-```
-
-If the LLM returns invalid JSON or missing fields, the grading fails and returns score 0.0 with an error message.
-
-## Agent-as-Judge
-
-Instead of calling an LLM API directly, you can use a **Letta agent** as the judge. The agent-as-judge approach loads a Letta agent from a `.af` file, sends it the evaluation criteria, and collects its score via a tool call.
-
-### Why Use Agent-as-Judge?
-
-Agent-as-judge is ideal when:
-
-1. **No direct LLM API access**: Your team uses Letta Cloud or managed instances without direct API keys
-2. **Judges need tools**: The evaluator needs to call tools during grading (e.g., web search, database queries, fetching webpages to verify answers)
-3. **Centralized LLM access**: Your organization provides LLM access only through Letta
-4. **Custom evaluation logic**: You want the judge to use specific tools or follow complex evaluation workflows
-5. **Teacher-student patterns**: You have a well-built, experienced agent that can evaluate and teach a student agent being developed
-
-### Configuration
-
-To use agent-as-judge, specify `agent_file` instead of `model`:
-
-```yaml
-graders:
- agent_judge:
- kind: rubric # Still "rubric" kind
- agent_file: judge.af # Path to judge agent .af file
- prompt_path: rubric.txt # Evaluation criteria
- judge_tool_name: submit_grade # Tool for submitting scores (default: submit_grade)
- extractor: last_assistant # What to extract from target agent
-```
-
-**Key differences from standard rubric grading:**
-- Use `agent_file` instead of `model`
-- No `temperature`, `provider`, `max_retries`, or `timeout` fields (agent handles retries internally)
-- Judge agent must have a `submit_grade(score: float, rationale: str)` tool
-- Framework validates judge tool on initialization (fail-fast)
-
-### Judge Agent Requirements
-
-Your judge agent **must** have a tool with this exact signature:
-
-```python
-def submit_grade(score: float, rationale: str) -> dict:
- """
- Submit an evaluation grade for an agent's response.
-
- Args:
- score: A float between 0.0 (complete failure) and 1.0 (perfect)
- rationale: Explanation of why this score was given
-
- Returns:
- dict: Confirmation of grade submission
- """
- return {
- "status": "success",
- "grade": {"score": score, "rationale": rationale}
- }
-```
-
-**Validation on initialization**: The framework validates the judge agent has the correct tool with the right parameters **before** running evaluations. If validation fails, you'll get a clear error:
-
-```
-ValueError: Judge tool 'submit_grade' not found in agent file judge.af.
-Available tools: ['fetch_webpage', 'search_documents']
-```
-
-This fail-fast approach catches configuration errors immediately.
-
-### Checklist: Will Your Judge Agent Work?
-
-- [ ] **Tool exists**: Agent has a tool with the name specified in `judge_tool_name` (default: `submit_grade`)
-- [ ] **Tool parameters**: The tool has BOTH `score: float` and `rationale: str` parameters
-- [ ] **Tool is callable**: The tool is not disabled or requires-approval-only
-- [ ] **Agent system prompt**: Agent understands it's an evaluator (optional but recommended)
-- [ ] **No conflicting tools**: Agent doesn't have other tools that might confuse it into answering questions instead of judging
-
-### Example Configuration
-
-**suite.yaml:**
-```yaml
-name: fetch-webpage-agent-judge-test
-description: Test agent responses using a Letta agent as judge
-dataset: dataset.csv
-
-target:
- kind: agent
- agent_file: my_agent.af # Agent being tested
- base_url: http://localhost:8283
-
-graders:
- agent_judge:
- kind: rubric
- agent_file: judge.af # Judge agent with submit_grade tool
- prompt_path: rubric.txt # Evaluation criteria
- judge_tool_name: submit_grade # Tool name (default: submit_grade)
- extractor: last_assistant # Extract target agent's response
-
-gate:
- metric_key: agent_judge
- op: gte
- value: 0.75 # Pass if avg score ≥ 0.75
-```
-
-**rubric.txt:**
-```
-Evaluate the agent's response based on the following criteria:
-
-1. **Correctness (0.6 weight)**: Does the response contain accurate information from the webpage? Check if the answer matches what was requested in the input.
-
-2. **Format (0.2 weight)**: Is the response formatted correctly? The input often requests answers in a specific format (e.g., in brackets like {Answer}).
-
-3. **Completeness (0.2 weight)**: Does the response fully address the question without unnecessary information?
-
-Scoring Guidelines:
-- 1.0: Perfect response - correct, properly formatted, and complete
-- 0.75-0.99: Good response - minor formatting or completeness issues
-- 0.5-0.74: Adequate response - correct information but format/completeness problems
-- 0.25-0.49: Poor response - partially correct or missing key information
-- 0.0-0.24: Failed response - incorrect or no relevant information
-
-Use the submit_grade tool to submit your evaluation with a score between 0.0 and 1.0. You will need to use your fetch_webpage tool to fetch the desired webpage and confirm the answer is correct.
-```
-
-**Judge agent with tools**: The judge agent in this example has `fetch_webpage` tool, allowing it to independently verify answers by fetching the webpage mentioned in the input.
-
-### How Agent-as-Judge Works
-
-1. **Agent Loading**: Loads judge agent from `.af` file and validates tool signature
-2. **Prompt Formatting**: Formats the rubric with `{input}`, `{submission}`, `{ground_truth}` placeholders
-3. **Agent Evaluation**: Sends formatted prompt to judge agent as a message
-4. **Tool Call Parsing**: Extracts score and rationale from `submit_grade` tool call
-5. **Cleanup**: Deletes judge agent after evaluation to free resources
-6. **Error Handling**: Returns score 0.0 with error message if judge fails to call the tool
-
-### Agent-as-Judge vs Standard Rubric Grading
-
-| Feature | Standard Rubric | Agent-as-Judge |
-|---------|----------------|----------------|
-| **LLM Access** | Direct API (OPENAI_API_KEY) | Through Letta agent |
-| **Tools** | No tool usage | Judge can use tools |
-| **Configuration** | `model`, `temperature`, etc. | `agent_file`, `judge_tool_name` |
-| **Output Format** | JSON structured output | Tool call with score/rationale |
-| **Validation** | Runtime JSON parsing | Upfront tool signature validation |
-| **Use Case** | Teams with API access | Teams using Letta Cloud, judges needing tools |
-| **Cost** | API call per sample | Depends on judge agent's LLM config |
-
-### Teacher-Student Pattern
-
-A powerful use case for agent-as-judge is the **teacher-student pattern**, where an experienced, well-configured agent evaluates a student agent being developed.
-
-> **Prerequisites**: This pattern assumes you already have a well-defined, production-ready agent that performs well on your task. This agent becomes the "teacher" that evaluates the "student" agent you're developing.
-
-**Why this works:**
-- **Domain expertise**: The teacher agent has specialized knowledge and tools
-- **Consistent evaluation**: The teacher applies the same standards across all evaluations
-- **Tool-based verification**: The teacher can independently verify answers using its own tools
-- **Iterative improvement**: Use the teacher to evaluate multiple versions of the student as you improve it
-
-**Example scenario:**
-You have a production-ready customer support agent with domain expertise and access to your tools (knowledge base, CRM, documentation search, etc.). You're developing a new, faster version of this agent. Use the experienced agent as the judge to evaluate whether the new agent meets the same quality standards.
-
-**Configuration:**
-```yaml
-name: student-agent-evaluation
-description: Experienced agent evaluates student agent performance
-dataset: support_questions.csv
-
-target:
- kind: agent
- agent_file: student_agent.af # New agent being developed
- base_url: http://localhost:8283
-
-graders:
- teacher_evaluation:
- kind: rubric
- agent_file: teacher_agent.af # Experienced production agent with domain tools
- prompt: |
- You are an experienced customer support agent evaluating a new agent's response.
-
- Customer question: {input}
- Student agent's answer: {submission}
-
- Use your available tools to verify the answer is correct and complete.
- Grade based on:
- 1. Factual accuracy (0.5 weight) - Does the answer contain correct information?
- 2. Completeness (0.3 weight) - Does it fully address the question?
- 3. Tone and professionalism (0.2 weight) - Is it appropriately worded?
-
- Submit a score from 0.0 to 1.0 using the submit_grade tool.
- extractor: last_assistant
-
-gate:
- metric_key: teacher_evaluation
- op: gte
- value: 0.8 # Student must score 80% or higher
-```
-
-**Benefits of this approach:**
-- **Leverage existing expertise**: Your best agent becomes the standard
-- **Scalable quality control**: Teacher evaluates hundreds of scenarios automatically
-- **Continuous validation**: Run teacher evaluations in CI/CD as you iterate on the student
-- **Transfer learning**: Teacher's evaluation helps identify where the student needs improvement
-
-### Complete Example
-
-See [`examples/letta-agent-rubric-grader/`](https://github.com/letta-ai/letta-evals/tree/main/examples/letta-agent-rubric-grader) for a working example with:
-- Judge agent with `submit_grade` and `fetch_webpage` tools
-- Target agent that fetches webpages and answers questions
-- Rubric that instructs judge to verify answers independently
-- Complete suite configuration
-
-## Use Cases
-
-### Quality Assessment
-
-```yaml
-graders:
- quality:
- kind: rubric
- prompt_path: quality_rubric.txt
- model: gpt-4o-mini
- extractor: last_assistant
-```
-
-`quality_rubric.txt`:
-```
-Evaluate response quality based on:
-1. Accuracy of information
-2. Completeness of answer
-3. Clarity of explanation
-
-Response: {submission}
-Ground truth: {ground_truth}
-
-Score from 0.0 to 1.0.
-```
-
-### Creativity Evaluation
-
-```yaml
-graders:
- creativity:
- kind: rubric
- prompt: |
- Rate the creativity and originality of this story.
-
- Story: {submission}
-
- 1.0 = Highly creative and original
- 0.5 = Some creative elements
- 0.0 = Generic or cliché
- model: gpt-4o-mini
- extractor: last_assistant
-```
-
-### Multi-Criteria Evaluation
-
-```yaml
-graders:
- comprehensive:
- kind: rubric
- prompt: |
- Evaluate the response on multiple criteria:
-
- 1. Technical Accuracy (40%)
- 2. Clarity of Explanation (30%)
- 3. Completeness (20%)
- 4. Conciseness (10%)
-
- Input: {input}
- Response: {submission}
- Expected: {ground_truth}
-
- Provide a weighted score from 0.0 to 1.0.
- model: gpt-4o
- extractor: last_assistant
-```
-
-### Code Quality
-
-```yaml
-graders:
- code_quality:
- kind: rubric
- prompt: |
- Evaluate this code for:
- - Correctness
- - Readability
- - Efficiency
- - Best practices
-
- Code: {submission}
-
- Score from 0.0 to 1.0.
- model: gpt-4o
- extractor: last_assistant
-```
-
-### Tone and Style
-
-```yaml
-graders:
- professionalism:
- kind: rubric
- prompt: |
- Rate the professionalism and appropriate tone of the response.
-
- Response: {submission}
-
- 1.0 = Highly professional
- 0.5 = Acceptable
- 0.0 = Unprofessional or inappropriate
- model: gpt-4o-mini
- extractor: last_assistant
-```
-
-## Best Practices
-
-### 1. Clear Scoring Criteria
-
-Provide explicit score ranges and what they mean:
-
-```
-Score:
-- 1.0: Perfect response with no issues
-- 0.8-0.9: Minor improvements possible
-- 0.6-0.7: Some gaps or errors
-- 0.4-0.5: Significant problems
-- 0.2-0.3: Major issues
-- 0.0-0.1: Complete failure
-```
-
-### 2. Use Ground Truth When Available
-
-If you have expected answers, include them:
-
-```
-Expected: {ground_truth}
-Actual: {submission}
-
-Evaluate how well the actual response matches the expected content.
-```
-
-### 3. Be Specific About Criteria
-
-Vague: "Evaluate the quality"
-Better: "Evaluate accuracy, completeness, and clarity"
-
-### 4. Use Examples in Rubric
-
-```
-Example of 1.0: "A complete, accurate answer with clear explanation"
-Example of 0.5: "Partially correct but missing key details"
-Example of 0.0: "Incorrect or irrelevant response"
-```
-
-### 5. Calibrate with Test Cases
-
-Run on a small set first to ensure the rubric produces expected scores.
-
-### 6. Consider Model Choice
-
-- **gpt-4o-mini**: Fast and cost-effective for simple criteria
-- **gpt-4o**: More accurate for complex evaluation
-- **claude-3-5-sonnet**: Alternative perspective (via OpenAI-compatible endpoint)
-
-## Environment Setup
-
-Rubric graders require an OpenAI API key:
-
-```bash
-export OPENAI_API_KEY=your-api-key
-```
-
-For custom endpoints:
-
-```bash
-export OPENAI_BASE_URL=https://your-endpoint.com/v1
-```
-
-## Error Handling
-
-If grading fails:
-- Score is set to 0.0
-- Rationale includes error message
-- Metadata includes error details
-- Evaluation continues (doesn't stop the suite)
-
-Common errors:
-- API timeout → Check `timeout` setting
-- Invalid API key → Verify `OPENAI_API_KEY`
-- Rate limit → Reduce concurrency or add retries
-
-## Cost Considerations
-
-Rubric graders make API calls for each sample:
-
-- **gpt-4o-mini**: ~$0.00015 per evaluation (cheap)
-- **gpt-4o**: ~$0.002 per evaluation (more expensive)
-
-For 1000 samples:
-- gpt-4o-mini: ~$0.15
-- gpt-4o: ~$2.00
-
-Estimate costs before running large evaluations.
-
-## Performance
-
-Rubric graders are slower than tool graders:
-- Tool grader: <1ms per sample
-- Rubric grader: 500-2000ms per sample (network + LLM)
-
-Use concurrency to speed up:
-
-```bash
-letta-evals run suite.yaml --max-concurrent 10
-```
-
-## Limitations
-
-Rubric graders:
-- **Cost**: API calls cost money
-- **Speed**: Slower than tool graders
-- **Consistency**: Can vary slightly between runs (use temperature 0.0 for best consistency)
-- **API dependency**: Requires network and API availability
-
-For deterministic, fast evaluation, use [Tool Graders](./tool-graders.md).
-
-## Combining Tool and Rubric Graders
-
-Use both in one suite:
-
-```yaml
-graders:
- format_check:
- kind: tool
- function: regex_match
- extractor: last_assistant
-
- quality:
- kind: rubric
- prompt_path: quality.txt
- model: gpt-4o-mini
- extractor: last_assistant
-
-gate:
- metric_key: quality # Gate on quality, but still check format
- op: gte
- value: 0.7
-```
-
-This combines fast deterministic checks with nuanced quality evaluation.
-
-## Next Steps
-
-- [Built-in Extractors](../extractors/builtin.md) - Understanding what to extract from trajectories
-- [Tool Graders](./tool-graders.md) - Deterministic evaluation for objective criteria
-- [Multi-Metric Evaluation](./multi-metric.md) - Combining multiple graders
-- [Custom Graders](../advanced/custom-graders.md) - Writing custom evaluation logic
diff --git a/fern/pages/evals/graders/tool-graders.md b/fern/pages/evals/graders/tool-graders.md
deleted file mode 100644
index 3e9fdeb0..00000000
--- a/fern/pages/evals/graders/tool-graders.md
+++ /dev/null
@@ -1,332 +0,0 @@
-# Tool Graders
-
-Tool graders use Python functions to programmatically evaluate submissions. They're ideal for deterministic, rule-based evaluation.
-
-## Overview
-
-Tool graders:
-- Execute Python functions that take `(sample, submission)` and return a `GradeResult`
-- Are fast and deterministic
-- Don't require external API calls
-- Can implement any custom logic
-
-## Configuration
-
-```yaml
-graders:
- my_metric:
- kind: tool
- function: exact_match # Function name
- extractor: last_assistant # What to extract from trajectory
-```
-
-The `extractor` determines what part of the agent's response to evaluate. See [Built-in Extractors](../extractors/builtin.md) for all available options.
-
-## Built-in Functions
-
-### exact_match
-
-Exact string comparison (case-sensitive, whitespace-trimmed).
-
-```yaml
-graders:
- accuracy:
- kind: tool
- function: exact_match
- extractor: last_assistant
-```
-
-**Requires**: `ground_truth` in dataset
-
-**Returns**:
-- Score: 1.0 if exact match, 0.0 otherwise
-- Rationale: "Exact match: true" or "Exact match: false"
-
-**Example**:
-```jsonl
-{"input": "What is 2+2?", "ground_truth": "4"}
-```
-
-Submission "4" → Score 1.0
-Submission "four" → Score 0.0
-
-### contains
-
-Case-insensitive substring check.
-
-```yaml
-graders:
- keyword_check:
- kind: tool
- function: contains
- extractor: last_assistant
-```
-
-**Requires**: `ground_truth` in dataset
-
-**Returns**:
-- Score: 1.0 if ground_truth found in submission (case-insensitive), 0.0 otherwise
-- Rationale: "Contains ground_truth: true" or "Contains ground_truth: false"
-
-**Example**:
-```jsonl
-{"input": "What is the capital of France?", "ground_truth": "Paris"}
-```
-
-Submission "The capital is Paris" → Score 1.0
-Submission "The capital is paris" → Score 1.0 (case-insensitive)
-Submission "The capital is Lyon" → Score 0.0
-
-### regex_match
-
-Pattern matching using regex.
-
-```yaml
-graders:
- pattern_check:
- kind: tool
- function: regex_match
- extractor: last_assistant
-```
-
-**Requires**: `ground_truth` in dataset (as regex pattern)
-
-**Returns**:
-- Score: 1.0 if pattern matches, 0.0 otherwise
-- Rationale: "Regex match: true" or "Regex match: false"
-- If pattern is invalid: Score 0.0 with error message
-
-**Example**:
-```jsonl
-{"input": "Generate a UUID", "ground_truth": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"}
-{"input": "Extract the number", "ground_truth": "\\d+"}
-```
-
-Submission "550e8400-e29b-41d4-a716-446655440000" → Score 1.0
-Submission "not-a-uuid" → Score 0.0
-
-### ascii_printable_only
-
-Validates that all characters are printable ASCII (code points 32-126).
-
-```yaml
-graders:
- ascii_check:
- kind: tool
- function: ascii_printable_only
- extractor: last_assistant
-```
-
-**Requires**: No ground_truth needed
-
-**Returns**:
-- Score: 1.0 if all characters are printable ASCII, 0.0 if any non-printable found
-- Rationale: Details about non-printable characters if found
-
-**Notes**:
-- Newlines (`\n`) and carriage returns (`\r`) are ignored (allowed)
-- Useful for ASCII art, formatted output, or ensuring clean text
-
-**Example**:
-
-Submission "Hello, World!\n" → Score 1.0
-Submission "Hello 🌍" → Score 0.0 (emoji not in ASCII range)
-
-## Custom Tool Graders
-
-You can write custom grading functions:
-
-```python
-# custom_graders.py
-from letta_evals.decorators import grader
-from letta_evals.models import GradeResult, Sample
-
-@grader
-def my_custom_grader(sample: Sample, submission: str) -> GradeResult:
- """Custom grading logic."""
- # Your evaluation logic here
- score = 1.0 if some_condition(submission) else 0.0
- return GradeResult(
- score=score,
- rationale=f"Explanation of the score",
- metadata={"extra": "info"}
- )
-```
-
-Then reference it in your suite:
-
-```yaml
-graders:
- custom:
- kind: tool
- function: my_custom_grader
- extractor: last_assistant
-```
-
-See [Custom Graders](../advanced/custom-graders.md) for details.
-
-## Use Cases
-
-### Exact Answer Validation
-
-```yaml
-graders:
- correct_answer:
- kind: tool
- function: exact_match
- extractor: last_assistant
-```
-
-Best for: Math problems, single-word answers, structured formats
-
-### Keyword Presence
-
-```yaml
-graders:
- mentions_topic:
- kind: tool
- function: contains
- extractor: last_assistant
-```
-
-Best for: Checking if specific concepts are mentioned
-
-### Format Validation
-
-```yaml
-graders:
- valid_email:
- kind: tool
- function: regex_match
- extractor: last_assistant
-```
-
-Dataset:
-```jsonl
-{"input": "Extract the email", "ground_truth": "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"}
-```
-
-Best for: Emails, UUIDs, phone numbers, structured data
-
-### Tool Call Validation
-
-```yaml
-graders:
- used_search:
- kind: tool
- function: contains
- extractor: tool_arguments
- extractor_config:
- tool_name: search
-```
-
-Dataset:
-```jsonl
-{"input": "Find information about pandas", "ground_truth": "pandas"}
-```
-
-Checks if the agent called the search tool with "pandas" in arguments.
-
-### JSON Structure Validation
-
-Custom grader:
-
-```python
-import json
-from letta_evals.decorators import grader
-from letta_evals.models import GradeResult, Sample
-
-@grader
-def valid_json_with_field(sample: Sample, submission: str) -> GradeResult:
- try:
- data = json.loads(submission)
- required_field = sample.ground_truth
- if required_field in data:
- return GradeResult(score=1.0, rationale=f"Valid JSON with '{required_field}' field")
- else:
- return GradeResult(score=0.0, rationale=f"Missing required field: {required_field}")
- except json.JSONDecodeError as e:
- return GradeResult(score=0.0, rationale=f"Invalid JSON: {e}")
-```
-
-## Combining with Extractors
-
-Tool graders work with any extractor:
-
-### Grade Tool Arguments
-
-```yaml
-graders:
- correct_tool:
- kind: tool
- function: exact_match
- extractor: tool_arguments
- extractor_config:
- tool_name: calculator
-```
-
-Checks if calculator was called with specific arguments.
-
-### Grade Memory Updates
-
-```yaml
-graders:
- memory_correct:
- kind: tool
- function: contains
- extractor: memory_block
- extractor_config:
- block_label: human
-```
-
-Checks if agent's memory block contains expected content.
-
-### Grade Pattern Extraction
-
-```yaml
-graders:
- extracted_correctly:
- kind: tool
- function: exact_match
- extractor: pattern
- extractor_config:
- pattern: 'ANSWER: (.*)'
- group: 1
-```
-
-Extracts content after "ANSWER:" and checks if it matches ground truth.
-
-## Performance
-
-Tool graders are:
-- **Fast**: No API calls, pure Python execution
-- **Deterministic**: Same input always produces same result
-- **Cost-effective**: No LLM API costs
-- **Reliable**: No network dependencies
-
-Use tool graders when possible for faster, cheaper evaluations.
-
-## Limitations
-
-Tool graders:
-- Can't evaluate subjective quality
-- Limited to predefined logic
-- Don't understand semantic similarity
-- Can't handle complex, nuanced criteria
-
-For these cases, use [Rubric Graders](./rubric-graders.md).
-
-## Best Practices
-
-1. **Use exact_match for precise answers**: Math, single words, structured formats
-2. **Use contains for flexible matching**: When exact format varies but key content is present
-3. **Use regex for format validation**: Emails, phone numbers, UUIDs
-4. **Write custom graders for complex logic**: Multi-step validation, JSON parsing
-5. **Combine multiple graders**: Evaluate different aspects (format + content + tool usage)
-
-## Next Steps
-
-- [Built-in Extractors](../extractors/builtin.md) - Understanding what to extract from trajectories
-- [Rubric Graders](./rubric-graders.md) - LLM-based evaluation for subjective quality
-- [Custom Graders](../advanced/custom-graders.md) - Writing your own grading functions
-- [Multi-Metric Evaluation](./multi-metric.md) - Using multiple graders simultaneously
diff --git a/fern/pages/evals/results/overview.md b/fern/pages/evals/results/overview.md
deleted file mode 100644
index 9e80f7ea..00000000
--- a/fern/pages/evals/results/overview.md
+++ /dev/null
@@ -1,468 +0,0 @@
-# Understanding Results
-
-This guide explains how to interpret evaluation results.
-
-## Result Structure
-
-An evaluation produces three types of output:
-
-1. **Console output**: Real-time progress and summary
-2. **Summary JSON**: Aggregate metrics and configuration
-3. **Results JSONL**: Per-sample detailed results
-
-## Console Output
-
-### Progress Display
-
-```
-Running evaluation: my-eval-suite
-━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3/3 100%
-
-Results:
- Total samples: 3
- Attempted: 3
- Avg score: 0.83 (attempted: 0.83)
- Passed: 2 (66.7%)
-
-Gate (quality >= 0.75): PASSED
-```
-
-### Quiet Mode
-
-```bash
-letta-evals run suite.yaml --quiet
-```
-
-Output:
-```
-✓ PASSED
-```
-
-or
-
-```
-✗ FAILED
-```
-
-## JSON Output
-
-### Saving Results
-
-```bash
-letta-evals run suite.yaml --output results/
-```
-
-Creates three files:
-
-#### header.json
-
-Evaluation metadata:
-
-```json
-{
- "suite_name": "my-eval-suite",
- "timestamp": "2025-01-15T10:30:00Z",
- "version": "0.3.0"
-}
-```
-
-#### summary.json
-
-Complete evaluation summary:
-
-```json
-{
- "suite": "my-eval-suite",
- "config": {
- "target": {...},
- "graders": {...},
- "gate": {...}
- },
- "metrics": {
- "total": 10,
- "total_attempted": 10,
- "avg_score_attempted": 0.85,
- "avg_score_total": 0.85,
- "passed_attempts": 8,
- "failed_attempts": 2,
- "by_metric": {
- "accuracy": {
- "avg_score_attempted": 0.90,
- "pass_rate": 90.0,
- "passed_attempts": 9,
- "failed_attempts": 1
- },
- "quality": {
- "avg_score_attempted": 0.80,
- "pass_rate": 70.0,
- "passed_attempts": 7,
- "failed_attempts": 3
- }
- }
- },
- "gates_passed": true
-}
-```
-
-#### results.jsonl
-
-One JSON object per line, each representing one sample:
-
-```jsonl
-{"sample": {"id": 0, "input": "What is 2+2?", "ground_truth": "4"}, "submission": "4", "grade": {"score": 1.0, "rationale": "Exact match: true"}, "trajectory": [...], "agent_id": "agent-123", "model_name": "default"}
-{"sample": {"id": 1, "input": "What is 3+3?", "ground_truth": "6"}, "submission": "6", "grade": {"score": 1.0, "rationale": "Exact match: true"}, "trajectory": [...], "agent_id": "agent-124", "model_name": "default"}
-```
-
-## Metrics Explained
-
-### total
-
-Total number of samples in the evaluation (including errors).
-
-### total_attempted
-
-Number of samples that completed without errors.
-
-If a sample fails during agent execution or grading, it's counted in `total` but not `total_attempted`.
-
-### avg_score_attempted
-
-Average score across samples that completed successfully.
-
-Formula: `sum(scores) / total_attempted`
-
-Range: 0.0 to 1.0
-
-### avg_score_total
-
-Average score across all samples, treating errors as 0.0.
-
-Formula: `sum(scores) / total`
-
-Range: 0.0 to 1.0
-
-### passed_attempts / failed_attempts
-
-Number of samples that passed/failed the gate's per-sample criteria.
-
-By default:
-- If gate metric is `accuracy`: sample passes if score >= 1.0
-- If gate metric is `avg_score`: sample passes if score >= gate value
-
-Can be customized with `pass_op` and `pass_value` in gate config.
-
-### by_metric
-
-For multi-metric evaluation, shows aggregate stats for each metric:
-
-```json
-"by_metric": {
- "accuracy": {
- "avg_score_attempted": 0.90,
- "avg_score_total": 0.85,
- "pass_rate": 90.0,
- "passed_attempts": 9,
- "failed_attempts": 1
- }
-}
-```
-
-## Sample Results
-
-Each sample result includes:
-
-### sample
-The original dataset sample:
-```json
-"sample": {
- "id": 0,
- "input": "What is 2+2?",
- "ground_truth": "4",
- "metadata": {...}
-}
-```
-
-### submission
-The extracted text that was graded:
-```json
-"submission": "The answer is 4"
-```
-
-### grade
-The grading result:
-```json
-"grade": {
- "score": 1.0,
- "rationale": "Contains ground_truth: true",
- "metadata": {"model": "gpt-4o-mini", "usage": {...}}
-}
-```
-
-### grades (multi-metric)
-For multi-metric evaluation:
-```json
-"grades": {
- "accuracy": {"score": 1.0, "rationale": "Exact match"},
- "quality": {"score": 0.85, "rationale": "Good but verbose"}
-}
-```
-
-### trajectory
-The complete conversation history:
-```json
-"trajectory": [
- [
- {"role": "user", "content": "What is 2+2?"},
- {"role": "assistant", "content": "The answer is 4"}
- ]
-]
-```
-
-### agent_id
-The ID of the agent that generated this response:
-```json
-"agent_id": "agent-abc-123"
-```
-
-### model_name
-The model configuration used:
-```json
-"model_name": "gpt-4o-mini"
-```
-
-### agent_usage
-Token usage statistics (if available):
-```json
-"agent_usage": [
- {"completion_tokens": 10, "prompt_tokens": 50, "total_tokens": 60}
-]
-```
-
-## Interpreting Scores
-
-### Score Ranges
-
-- **1.0**: Perfect - fully meets criteria
-- **0.8-0.99**: Very good - minor issues
-- **0.6-0.79**: Good - notable improvements possible
-- **0.4-0.59**: Acceptable - significant issues
-- **0.2-0.39**: Poor - major problems
-- **0.0-0.19**: Failed - did not meet criteria
-
-### Binary vs Continuous
-
-**Tool graders** typically return binary scores:
-- 1.0: Passed
-- 0.0: Failed
-
-**Rubric graders** return continuous scores:
-- Any value from 0.0 to 1.0
-- Allows for partial credit
-
-## Multi-Model Results
-
-When testing multiple models:
-
-```json
-"metrics": {
- "per_model": [
- {
- "model_name": "gpt-4o-mini",
- "avg_score_attempted": 0.85,
- "passed_samples": 8,
- "failed_samples": 2
- },
- {
- "model_name": "claude-3-5-sonnet",
- "avg_score_attempted": 0.90,
- "passed_samples": 9,
- "failed_samples": 1
- }
- ]
-}
-```
-
-Console output:
-```
-Results by model:
- gpt-4o-mini - Avg: 0.85, Pass: 80.0%
- claude-3-5-sonnet - Avg: 0.90, Pass: 90.0%
-```
-
-## Multiple Runs Statistics
-
-Run evaluations multiple times to measure consistency and get aggregate statistics.
-
-### Configuration
-
-Specify in YAML:
-```yaml
-name: my-eval-suite
-dataset: dataset.jsonl
-num_runs: 5 # Run 5 times
-target:
- kind: agent
- agent_file: my_agent.af
-graders:
- accuracy:
- kind: tool
- function: exact_match
-gate:
- metric_key: accuracy
- op: gte
- value: 0.8
-```
-
-Or via CLI:
-```bash
-letta-evals run suite.yaml --num-runs 10 --output results/
-```
-
-### Output Structure
-
-```
-results/
-├── run_1/
-│ ├── header.json
-│ ├── results.jsonl
-│ └── summary.json
-├── run_2/
-│ ├── header.json
-│ ├── results.jsonl
-│ └── summary.json
-├── ...
-└── aggregate_stats.json # Statistics across all runs
-```
-
-### Aggregate Statistics File
-
-The `aggregate_stats.json` includes statistics across all runs:
-
-```json
-{
- "num_runs": 10,
- "runs_passed": 8,
- "runs_failed": 2,
- "pass_rate": 80.0,
- "avg_score_attempted": {
- "mean": 0.847,
- "std": 0.042,
- "min": 0.78,
- "max": 0.91
- },
- "avg_score_total": {
- "mean": 0.847,
- "std": 0.042,
- "min": 0.78,
- "max": 0.91
- },
- "per_metric": {
- "accuracy": {
- "avg_score_attempted": {
- "mean": 0.89,
- "std": 0.035,
- "min": 0.82,
- "max": 0.95
- },
- "pass_rate": {
- "mean": 89.0,
- "std": 4.2,
- "min": 80.0,
- "max": 95.0
- }
- }
- }
-}
-```
-
-### Use Cases
-
-**Measure consistency of non-deterministic agents:**
-```bash
-letta-evals run suite.yaml --num-runs 20 --output results/
-# Check stddev in aggregate_stats.json
-# Low stddev = consistent, high stddev = variable
-```
-
-**Get confidence intervals:**
-```python
-import json
-import math
-
-with open("results/aggregate_stats.json") as f:
- stats = json.load(f)
-
-mean = stats["avg_score_attempted"]["mean"]
-std = stats["avg_score_attempted"]["std"]
-n = stats["num_runs"]
-
-# 95% confidence interval (assuming normal distribution)
-margin = 1.96 * (std / math.sqrt(n))
-print(f"Score: {mean:.3f} ± {margin:.3f}")
-```
-
-## Error Handling
-
-If a sample encounters an error:
-
-```json
-{
- "sample": {...},
- "submission": "",
- "grade": {
- "score": 0.0,
- "rationale": "Error during grading: Connection timeout",
- "metadata": {"error": "timeout", "error_type": "ConnectionError"}
- }
-}
-```
-
-Errors:
-- Count toward `total` but not `total_attempted`
-- Get score of 0.0
-- Include error details in rationale and metadata
-
-## Analyzing Results
-
-### Find Low Scores
-
-```python
-import json
-
-with open("results/results.jsonl") as f:
- results = [json.loads(line) for line in f]
-
-low_scores = [r for r in results if r["grade"]["score"] < 0.5]
-print(f"Found {len(low_scores)} samples with score < 0.5")
-
-for result in low_scores:
- print(f"Sample {result['sample']['id']}: {result['grade']['rationale']}")
-```
-
-### Compare Metrics
-
-```python
-# Load summary
-with open("results/summary.json") as f:
- summary = json.load(f)
-
-metrics = summary["metrics"]["by_metric"]
-for name, stats in metrics.items():
- print(f"{name}: {stats['avg_score_attempted']:.2f} avg, {stats['pass_rate']:.1f}% pass")
-```
-
-### Extract Failures
-
-```python
-# Find samples that failed gate criteria
-failures = [
- r for r in results
- if not gate_passed(r["grade"]["score"]) # Your gate logic
-]
-```
-
-## Next Steps
-
-- [Metrics Reference](./metrics.md)
-- [Output Formats](./output-formats.md)
-- [Best Practices](../best-practices/writing-tests.md)
diff --git a/fern/pages/evals/results/overview.mdx b/fern/pages/evals/results/overview.mdx
deleted file mode 100644
index ff99e399..00000000
--- a/fern/pages/evals/results/overview.mdx
+++ /dev/null
@@ -1,484 +0,0 @@
-# Understanding Results
-
-This guide explains how to interpret evaluation results.
-
-## Result Structure
-
-An evaluation produces three types of output:
-
-1. **Console output**: Real-time progress and summary
-2. **Summary JSON**: Aggregate metrics and configuration
-3. **Results JSONL**: Per-sample detailed results
-
-## Console Output
-
-### Progress Display
-
-```
-Running evaluation: my-eval-suite
-━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3/3 100%
-
-Results:
- Total samples: 3
- Attempted: 3
- Avg score: 0.83 (attempted: 0.83)
- Passed: 2 (66.7%)
-
-Gate (quality >= 0.75): PASSED
-```
-
-### Quiet Mode
-
-```bash
-letta-evals run suite.yaml --quiet
-```
-
-Output:
-```
-✓ PASSED
-```
-
-or
-
-```
-✗ FAILED
-```
-
-## JSON Output
-
-### Saving Results
-
-```bash
-letta-evals run suite.yaml --output results/
-```
-
-Creates three files:
-
-#### header.json
-
-Evaluation metadata:
-
-```json
-{
- "suite_name": "my-eval-suite",
- "timestamp": "2025-01-15T10:30:00Z",
- "version": "0.3.0"
-}
-```
-
-#### summary.json
-
-Complete evaluation summary:
-
-```json
-{
- "suite": "my-eval-suite",
- "config": {
- "target": {...},
- "graders": {...},
- "gate": {...}
- },
- "metrics": {
- "total": 10,
- "total_attempted": 10,
- "avg_score_attempted": 0.85,
- "avg_score_total": 0.85,
- "passed_attempts": 8,
- "failed_attempts": 2,
- "by_metric": {
- "accuracy": {
- "avg_score_attempted": 0.90,
- "pass_rate": 90.0,
- "passed_attempts": 9,
- "failed_attempts": 1
- },
- "quality": {
- "avg_score_attempted": 0.80,
- "pass_rate": 70.0,
- "passed_attempts": 7,
- "failed_attempts": 3
- }
- }
- },
- "gates_passed": true
-}
-```
-
-#### results.jsonl
-
-One JSON object per line, each representing one sample:
-
-```jsonl
-{"sample": {"id": 0, "input": "What is 2+2?", "ground_truth": "4"}, "submission": "4", "grade": {"score": 1.0, "rationale": "Exact match: true"}, "trajectory": [...], "agent_id": "agent-123", "model_name": "default"}
-{"sample": {"id": 1, "input": "What is 3+3?", "ground_truth": "6"}, "submission": "6", "grade": {"score": 1.0, "rationale": "Exact match: true"}, "trajectory": [...], "agent_id": "agent-124", "model_name": "default"}
-```
-
-## Metrics Explained
-
-### total
-
-Total number of samples in the evaluation (including errors).
-
-### total_attempted
-
-Number of samples that completed without errors.
-
-If a sample fails during agent execution or grading, it's counted in `total` but not `total_attempted`.
-
-### avg_score_attempted
-
-Average score across samples that completed successfully.
-
-Formula: `sum(scores) / total_attempted`
-
-Range: 0.0 to 1.0
-
-### avg_score_total
-
-Average score across all samples, treating errors as 0.0.
-
-Formula: `sum(scores) / total`
-
-Range: 0.0 to 1.0
-
-### passed_attempts / failed_attempts
-
-Number of samples that passed/failed the gate's per-sample criteria.
-
-By default:
-- If gate metric is `accuracy`: sample passes if score `>= 1.0`
-- If gate metric is `avg_score`: sample passes if score `>=` gate value
-
-Can be customized with `pass_op` and `pass_value` in gate config.
-
-### by_metric
-
-For multi-metric evaluation, shows aggregate stats for each metric:
-
-```json
-"by_metric": {
- "accuracy": {
- "avg_score_attempted": 0.90,
- "avg_score_total": 0.85,
- "pass_rate": 90.0,
- "passed_attempts": 9,
- "failed_attempts": 1
- }
-}
-```
-
-## Sample Results
-
-Each sample result includes:
-
-### sample
-The original dataset sample:
-```json
-"sample": {
- "id": 0,
- "input": "What is 2+2?",
- "ground_truth": "4",
- "metadata": {...}
-}
-```
-
-### submission
-The extracted text that was graded:
-```json
-"submission": "The answer is 4"
-```
-
-### grade
-The grading result:
-```json
-"grade": {
- "score": 1.0,
- "rationale": "Contains ground_truth: true",
- "metadata": {"model": "gpt-4o-mini", "usage": {...}}
-}
-```
-
-### grades (multi-metric)
-For multi-metric evaluation:
-```json
-"grades": {
- "accuracy": {"score": 1.0, "rationale": "Exact match"},
- "quality": {"score": 0.85, "rationale": "Good but verbose"}
-}
-```
-
-### trajectory
-The complete conversation history:
-```json
-"trajectory": [
- [
- {"role": "user", "content": "What is 2+2?"},
- {"role": "assistant", "content": "The answer is 4"}
- ]
-]
-```
-
-### agent_id
-The ID of the agent that generated this response:
-```json
-"agent_id": "agent-abc-123"
-```
-
-### model_name
-The model configuration used:
-```json
-"model_name": "gpt-4o-mini"
-```
-
-### agent_usage
-Token usage statistics (if available):
-```json
-"agent_usage": [
- {"completion_tokens": 10, "prompt_tokens": 50, "total_tokens": 60}
-]
-```
-
-## Interpreting Scores
-
-### Score Ranges
-
-- **1.0**: Perfect - fully meets criteria
-- **0.8-0.99**: Very good - minor issues
-- **0.6-0.79**: Good - notable improvements possible
-- **0.4-0.59**: Acceptable - significant issues
-- **0.2-0.39**: Poor - major problems
-- **0.0-0.19**: Failed - did not meet criteria
-
-### Binary vs Continuous
-
-**Tool graders** typically return binary scores:
-- 1.0: Passed
-- 0.0: Failed
-
-**Rubric graders** return continuous scores:
-- Any value from 0.0 to 1.0
-- Allows for partial credit
-
-## Multi-Model Results
-
-When testing multiple models:
-
-```json
-"metrics": {
- "per_model": [
- {
- "model_name": "gpt-4o-mini",
- "avg_score_attempted": 0.85,
- "passed_samples": 8,
- "failed_samples": 2
- },
- {
- "model_name": "claude-3-5-sonnet",
- "avg_score_attempted": 0.90,
- "passed_samples": 9,
- "failed_samples": 1
- }
- ]
-}
-```
-
-Console output:
-```
-Results by model:
- gpt-4o-mini - Avg: 0.85, Pass: 80.0%
- claude-3-5-sonnet - Avg: 0.90, Pass: 90.0%
-```
-
-## Multiple Runs Statistics
-
-Run evaluations multiple times to measure consistency and get aggregate statistics.
-
-### Configuration
-
-Specify in YAML:
-```yaml
-name: my-eval-suite
-dataset: dataset.jsonl
-num_runs: 5 # Run 5 times
-target:
- kind: agent
- agent_file: my_agent.af
-graders:
- accuracy:
- kind: tool
- function: exact_match
-gate:
- metric_key: accuracy
- op: gte
- value: 0.8
-```
-
-Or via CLI:
-```bash
-letta-evals run suite.yaml --num-runs 10 --output results/
-```
-
-### Output Structure
-
-```
-results/
-├── run_1/
-│ ├── header.json
-│ ├── results.jsonl
-│ └── summary.json
-├── run_2/
-│ ├── header.json
-│ ├── results.jsonl
-│ └── summary.json
-├── ...
-└── aggregate_stats.json # Statistics across all runs
-```
-
-### Aggregate Statistics File
-
-The `aggregate_stats.json` includes statistics across all runs:
-
-```json
-{
- "num_runs": 10,
- "runs_passed": 8,
- "mean_avg_score_attempted": 0.847,
- "std_avg_score_attempted": 0.042,
- "mean_avg_score_total": 0.847,
- "std_avg_score_total": 0.042,
- "mean_scores": {
- "accuracy": 0.89,
- "quality": 0.82
- },
- "std_scores": {
- "accuracy": 0.035,
- "quality": 0.051
- },
- "individual_run_metrics": [
- {
- "avg_score_attempted": 0.85,
- "avg_score_total": 0.85,
- "pass_rate": 0.85,
- "by_metric": {
- "accuracy": {
- "avg_score_attempted": 0.90,
- "avg_score_total": 0.90,
- "pass_rate": 0.90
- }
- }
- }
- // ... metrics from runs 2-10
- ]
-}
-```
-
-**Key fields**:
-- `num_runs`: Total number of runs executed
-- `runs_passed`: Number of runs that passed the gate
-- `mean_avg_score_attempted`: Mean score across runs (only attempted samples)
-- `std_avg_score_attempted`: Standard deviation (measures consistency)
-- `mean_scores`: Mean for each metric (e.g., `{"accuracy": 0.89}`)
-- `std_scores`: Standard deviation for each metric (e.g., `{"accuracy": 0.035}`)
-- `individual_run_metrics`: Full metrics object from each individual run
-
-### Use Cases
-
-**Measure consistency of non-deterministic agents:**
-```bash
-letta-evals run suite.yaml --num-runs 20 --output results/
-# Check std_avg_score_attempted in aggregate_stats.json
-# Low std = consistent, high std = variable
-```
-
-**Get confidence intervals:**
-```python
-import json
-import math
-
-with open("results/aggregate_stats.json") as f:
- stats = json.load(f)
-
-mean = stats["mean_avg_score_attempted"]
-std = stats["std_avg_score_attempted"]
-n = stats["num_runs"]
-
-# 95% confidence interval (assuming normal distribution)
-margin = 1.96 * (std / math.sqrt(n))
-print(f"Score: {mean:.3f} ± {margin:.3f}")
-```
-
-**Compare metric consistency:**
-```python
-with open("results/aggregate_stats.json") as f:
- stats = json.load(f)
-
-for metric_name, mean in stats["mean_scores"].items():
- std = stats["std_scores"][metric_name]
- consistency = "consistent" if std < 0.05 else "variable"
- print(f"{metric_name}: {mean:.3f} ± {std:.3f} ({consistency})")
-```
-
-## Error Handling
-
-If a sample encounters an error:
-
-```json
-{
- "sample": {...},
- "submission": "",
- "grade": {
- "score": 0.0,
- "rationale": "Error during grading: Connection timeout",
- "metadata": {"error": "timeout", "error_type": "ConnectionError"}
- }
-}
-```
-
-Errors:
-- Count toward `total` but not `total_attempted`
-- Get score of 0.0
-- Include error details in rationale and metadata
-
-## Analyzing Results
-
-### Find Low Scores
-
-```python
-import json
-
-with open("results/results.jsonl") as f:
- results = [json.loads(line) for line in f]
-
-low_scores = [r for r in results if r["grade"]["score"] < 0.5]
-print(f"Found {len(low_scores)} samples with score < 0.5")
-
-for result in low_scores:
- print(f"Sample {result['sample']['id']}: {result['grade']['rationale']}")
-```
-
-### Compare Metrics
-
-```python
-# Load summary
-with open("results/summary.json") as f:
- summary = json.load(f)
-
-metrics = summary["metrics"]["by_metric"]
-for name, stats in metrics.items():
- print(f"{name}: {stats['avg_score_attempted']:.2f} avg, {stats['pass_rate']:.1f}% pass")
-```
-
-### Extract Failures
-
-```python
-# Find samples that failed gate criteria
-failures = [
- r for r in results
- if not gate_passed(r["grade"]["score"]) # Your gate logic
-]
-```
-
-## Next Steps
-
-- [Gates](/guides/evals/concepts/gates) - Setting pass/fail criteria
-- [CLI Commands](/guides/evals/cli/commands) - Running evaluations
diff --git a/fern/pages/evals/troubleshooting.md b/fern/pages/evals/troubleshooting.md
deleted file mode 100644
index 8f9dd425..00000000
--- a/fern/pages/evals/troubleshooting.md
+++ /dev/null
@@ -1,438 +0,0 @@
-# Troubleshooting
-
-Common issues and solutions when using Letta Evals.
-
-## Installation Issues
-
-### "Command not found: letta-evals"
-
-**Problem**: CLI not available after installation
-
-**Solution**:
-```bash
-# Verify installation
-pip list | grep letta-evals
-
-# Reinstall if needed
-pip install --upgrade letta-evals
-
-# Or with uv
-uv sync
-```
-
-### Import errors
-
-**Problem**: `ModuleNotFoundError: No module named 'letta_evals'`
-
-**Solution**:
-```bash
-# Ensure you're in the right environment
-which python
-
-# Install in correct environment
-source .venv/bin/activate # or: ac
-pip install letta-evals
-```
-
-## Configuration Issues
-
-### "Agent file not found"
-
-**Problem**: `FileNotFoundError: agent.af`
-
-**Solution**:
-- Check the path is correct relative to the suite YAML
-- Use absolute paths if needed
-- Verify file exists: `ls -la path/to/agent.af`
-
-```yaml
-# Correct relative path
-target:
- agent_file: ./agents/my_agent.af
-
-# Or absolute path
-target:
- agent_file: /absolute/path/to/agent.af
-```
-
-### "Dataset not found"
-
-**Problem**: Cannot load dataset file
-
-**Solution**:
-- Verify dataset path in YAML
-- Check file exists: `ls -la dataset.jsonl`
-- Ensure proper JSONL format (one JSON object per line)
-
-```bash
-# Validate JSONL format
-cat dataset.jsonl | jq .
-```
-
-### "Validation failed: unknown function"
-
-**Problem**: Grader function not found
-
-**Solution**:
-```bash
-# List available graders
-letta-evals list-graders
-
-# Check spelling in suite.yaml
-graders:
- my_metric:
- function: exact_match # Correct
- # not: exactMatch or exact-match
-```
-
-### "Validation failed: unknown extractor"
-
-**Problem**: Extractor not found
-
-**Solution**:
-```bash
-# List available extractors
-letta-evals list-extractors
-
-# Check spelling
-graders:
- my_metric:
- extractor: last_assistant # Correct
- # not: lastAssistant or last-assistant
-```
-
-## Connection Issues
-
-### "Connection refused"
-
-**Problem**: Cannot connect to Letta server
-
-**Solution**:
-```bash
-# Verify server is running
-curl http://localhost:8283/v1/health
-
-# Check base_url in suite.yaml
-target:
- base_url: http://localhost:8283 # Correct port?
-
-# Or use environment variable
-export LETTA_BASE_URL=http://localhost:8283
-```
-
-### "Unauthorized" or "Invalid API key"
-
-**Problem**: Authentication failed
-
-**Solution**:
-```bash
-# Set API key
-export LETTA_API_KEY=your-key-here
-
-# Or in suite.yaml
-target:
- api_key: your-key-here
-
-# Verify key is correct
-echo $LETTA_API_KEY
-```
-
-### "Request timeout"
-
-**Problem**: Requests taking too long
-
-**Solution**:
-```yaml
-# Increase timeout
-target:
- timeout: 600.0 # 10 minutes
-
-# Rubric grader timeout
-graders:
- quality:
- kind: rubric
- timeout: 300.0 # 5 minutes
-```
-
-## Runtime Issues
-
-### "No ground_truth provided"
-
-**Problem**: Grader requires ground truth but sample doesn't have it
-
-**Solution**:
-- Add ground_truth to dataset samples:
-```jsonl
-{"input": "What is 2+2?", "ground_truth": "4"}
-```
-
-- Or use a grader that doesn't require ground truth:
-```yaml
-graders:
- quality:
- kind: rubric # Doesn't require ground_truth
- prompt_path: rubric.txt
-```
-
-### "Extractor requires agent_state"
-
-**Problem**: `memory_block` extractor needs agent state but it wasn't fetched
-
-**Solution**:
-This should be automatic, but if you see this error:
-- Check that the extractor is correctly configured
-- Ensure the agent exists and is accessible
-- Try using a different extractor if memory isn't needed
-
-### "Score must be between 0.0 and 1.0"
-
-**Problem**: Custom grader returning invalid score
-
-**Solution**:
-```python
-@grader
-def my_grader(sample, submission):
- score = calculate_score(submission)
- # Clamp score to valid range
- score = max(0.0, min(1.0, score))
- return GradeResult(score=score, rationale="...")
-```
-
-### "Invalid JSON in response"
-
-**Problem**: Rubric grader got non-JSON response
-
-**Solution**:
-- Check OpenAI API key is valid
-- Verify model name is correct
-- Check for network issues
-- Try increasing max_retries:
-```yaml
-graders:
- quality:
- kind: rubric
- max_retries: 10
-```
-
-## Performance Issues
-
-### Evaluation is very slow
-
-**Problem**: Taking too long to complete
-
-**Solutions**:
-
-1. Increase concurrency:
-```bash
-letta-evals run suite.yaml --max-concurrent 20
-```
-
-2. Reduce samples for testing:
-```yaml
-max_samples: 10 # Test with small subset first
-```
-
-3. Use tool graders instead of rubric graders when possible:
-```yaml
-graders:
- accuracy:
- kind: tool # Much faster than rubric
- function: exact_match
-```
-
-4. Check network latency:
-```bash
-# Test server response time
-time curl http://localhost:8283/v1/health
-```
-
-### High API costs
-
-**Problem**: Rubric graders costing too much
-
-**Solutions**:
-
-1. Use cheaper models:
-```yaml
-graders:
- quality:
- model: gpt-4o-mini # Cheaper than gpt-4o
-```
-
-2. Reduce number of rubric graders:
-```yaml
-graders:
- accuracy:
- kind: tool # Free
- quality:
- kind: rubric # Only use for subjective evaluation
-```
-
-3. Test with small sample first:
-```yaml
-max_samples: 5 # Verify before running full suite
-```
-
-## Results Issues
-
-### "No results generated"
-
-**Problem**: No output files created
-
-**Solution**:
-```bash
-# Specify output directory
-letta-evals run suite.yaml --output results/
-
-# Check for errors in console output
-letta-evals run suite.yaml # Without --quiet
-```
-
-### "All scores are 0.0"
-
-**Problem**: Everything failing
-
-**Solutions**:
-
-1. Check if agent is working:
-```bash
-# Test agent manually first
-```
-
-2. Verify extractor is getting content:
-- Add debug logging
-- Check sample results in output
-
-3. Check grader logic:
-```python
-# Test grader independently
-from letta_evals.models import Sample, GradeResult
-sample = Sample(id=0, input="test", ground_truth="test")
-result = my_grader(sample, "test")
-print(result)
-```
-
-### "Gates failed but scores look good"
-
-**Problem**: Passing samples but gate failing
-
-**Solution**:
-- Check gate configuration:
-```yaml
-gate:
- metric_key: accuracy # Correct metric?
- metric: avg_score # Or accuracy?
- op: gte # Correct operator?
- value: 0.8 # Correct threshold?
-```
-
-- Understand the difference between `avg_score` and `accuracy`
-- Check per-sample pass criteria with `pass_op` and `pass_value`
-
-## Environment Issues
-
-### "OPENAI_API_KEY not found"
-
-**Problem**: Rubric grader can't find API key
-
-**Solution**:
-```bash
-# Set in environment
-export OPENAI_API_KEY=your-key-here
-
-# Or in .env file
-echo "OPENAI_API_KEY=your-key-here" >> .env
-
-# Verify
-echo $OPENAI_API_KEY
-```
-
-### "Cannot use both model_configs and model_handles"
-
-**Problem**: Specified both in target config
-
-**Solution**:
-```yaml
-# Use one or the other, not both
-target:
- model_configs: [gpt-4o-mini] # For local server
- # OR
- model_handles: ["openai/gpt-4o-mini"] # For cloud
-```
-
-## Debug Tips
-
-### Enable verbose output
-
-Run without `--quiet` to see detailed progress:
-```bash
-letta-evals run suite.yaml
-```
-
-### Examine output files
-
-```bash
-letta-evals run suite.yaml --output debug/
-
-# Check summary
-cat debug/summary.json | jq .
-
-# Check individual results
-cat debug/results.jsonl | jq .
-```
-
-### Test with minimal suite
-
-Create a minimal test:
-```yaml
-name: debug-test
-dataset: test.jsonl # Just 1-2 samples
-
-target:
- kind: agent
- agent_file: agent.af
-
-graders:
- test:
- kind: tool
- function: contains
- extractor: last_assistant
-
-gate:
- op: gte
- value: 0.0 # Always pass
-```
-
-### Validate configuration
-
-```bash
-letta-evals validate suite.yaml
-```
-
-### Check component availability
-
-```bash
-letta-evals list-graders
-letta-evals list-extractors
-```
-
-## Getting Help
-
-If you're still stuck:
-
-1. Check the [documentation](./README.md)
-2. Look at [examples](../examples/)
-3. Report issues at https://github.com/anthropics/claude-code/issues
-
-When reporting issues, include:
-- Suite YAML configuration
-- Dataset sample (if not sensitive)
-- Error message and full stack trace
-- Output from `--output` directory
-- Environment info (OS, Python version)
-
-```bash
-# Get environment info
-python --version
-pip show letta-evals
-```
diff --git a/fern/pages/evals/troubleshooting.mdx b/fern/pages/evals/troubleshooting.mdx
deleted file mode 100644
index 7af7b782..00000000
--- a/fern/pages/evals/troubleshooting.mdx
+++ /dev/null
@@ -1,267 +0,0 @@
-# Troubleshooting
-
-Common issues and solutions when using Letta Evals.
-
-## Installation Issues
-
-
-**"Command not found: letta-evals"**
-
-**Problem**: CLI not available after installation
-
-**Solution**:
-```bash
-# Verify installation
-pip list | grep letta-evals
-
-# Reinstall if needed
-pip install --upgrade letta-evals
-```
-
-
-
-**Import errors**
-
-**Problem**: `ModuleNotFoundError: No module named 'letta_evals'`
-
-**Solution**:
-```bash
-# Ensure you're in the right environment
-which python
-
-# Install in correct environment
-source .venv/bin/activate
-pip install letta-evals
-```
-
-
-## Configuration Issues
-
-
-**"Agent file not found"**
-
-**Problem**: `FileNotFoundError: agent.af`
-
-**Solution**:
-- Check the path is correct relative to the suite YAML
-- Use absolute paths if needed
-- Verify file exists: `ls -la path/to/agent.af`
-
-```yaml
-# Correct relative path
-target:
- agent_file: ./agents/my_agent.af
-```
-
-
-
-**"Dataset not found"**
-
-**Problem**: Cannot load dataset file
-
-**Solution**:
-- Verify dataset path in YAML
-- Check file exists: `ls -la dataset.jsonl`
-- Ensure proper JSONL format (one JSON object per line)
-
-```bash
-# Validate JSONL format
-cat dataset.jsonl | jq .
-```
-
-
-
-**"Validation failed: unknown function"**
-
-**Problem**: Grader function not found
-
-**Solution**:
-```bash
-# List available graders
-letta-evals list-graders
-
-# Check spelling in suite.yaml
-graders:
- my_metric:
- function: exact_match # Correct
-```
-
-
-## Connection Issues
-
-
-**"Connection refused"**
-
-**Problem**: Cannot connect to Letta server
-
-**Solution**:
-```bash
-# Verify server is running
-curl https://api.letta.com/v1/health
-
-# Check base_url in suite.yaml
-target:
- base_url: https://api.letta.com
-```
-
-
-
-**"Unauthorized" or "Invalid API key"**
-
-**Problem**: Authentication failed
-
-**Solution**:
-```bash
-# Set API key
-export LETTA_API_KEY=your-key-here
-
-# Verify key is correct
-echo $LETTA_API_KEY
-```
-
-
-## Runtime Issues
-
-
-**"No ground_truth provided"**
-
-**Problem**: Grader requires ground truth but sample doesn't have it
-
-**Solution**:
-- Add ground_truth to dataset samples:
-```jsonl
-{"input": "What is 2+2?", "ground_truth": "4"}
-```
-
-- Or use a grader that doesn't require ground truth:
-```yaml
-graders:
- quality:
- kind: rubric # Doesn't require ground_truth
- prompt_path: rubric.txt
-```
-
-
-## Performance Issues
-
-
-**Evaluation is very slow**
-
-**Solutions**:
-
-1. Increase concurrency:
-```bash
-letta-evals run suite.yaml --max-concurrent 20
-```
-
-2. Reduce samples for testing:
-```yaml
-max_samples: 10 # Test with small subset first
-```
-
-3. Use tool graders instead of rubric graders:
-```yaml
-graders:
- accuracy:
- kind: tool # Much faster than rubric
- function: exact_match
-```
-
-
-
-**High API costs**
-
-**Solutions**:
-
-1. Use cheaper models:
-```yaml
-graders:
- quality:
- model: gpt-4o-mini # Cheaper than gpt-4o
-```
-
-2. Test with small sample first:
-```yaml
-max_samples: 5 # Verify before running full suite
-```
-
-
-## Results Issues
-
-
-**"All scores are 0.0"**
-
-**Solutions**:
-
-1. Verify extractor is getting content
-2. Check grader logic
-3. Test agent manually first
-
-
-
-**"Gates failed but scores look good"**
-
-**Solution**:
-- Check gate configuration:
-```yaml
-gate:
- metric_key: accuracy # Correct metric?
- metric: avg_score # Or accuracy?
- op: gte # Correct operator?
- value: 0.8 # Correct threshold?
-```
-
-
-## Debug Tips
-
-### Enable verbose output
-
-Run without `--quiet` to see detailed progress:
-```bash
-letta-evals run suite.yaml
-```
-
-### Examine output files
-
-```bash
-letta-evals run suite.yaml --output debug/
-
-# Check summary
-cat debug/summary.json | jq .
-
-# Check individual results
-cat debug/results.jsonl | jq .
-```
-
-### Validate configuration
-
-```bash
-letta-evals validate suite.yaml
-```
-
-### Check component availability
-
-```bash
-letta-evals list-graders
-letta-evals list-extractors
-```
-
-## Getting Help
-
-If you're still stuck:
-
-1. Check the [Getting Started guide](/evals/get-started/getting-started)
-2. Review the [Core Concepts](/evals/core-concepts/concepts-overview)
-3. Report issues at the [Letta Evals GitHub repository](https://github.com/letta-ai/letta-evals)
-
-When reporting issues, include:
-- Suite YAML configuration
-- Dataset sample (if not sensitive)
-- Error message and full stack trace
-- Environment info (OS, Python version)
-
-```bash
-# Get environment info
-python --version
-pip show letta-evals
-```
diff --git a/fern/pages/examples/hello_world.py b/fern/pages/examples/hello_world.py
deleted file mode 100644
index ffac5f0e..00000000
--- a/fern/pages/examples/hello_world.py
+++ /dev/null
@@ -1,48 +0,0 @@
-import os
-
-from letta_client import Letta
-
-# Initialize client (using LETTA_API_KEY environment variable)
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# Create agent
-agent = client.agents.create(
- name="hello_world_assistant",
- memory_blocks=[
- {"label": "persona", "value": "I am a friendly AI assistant here to help you learn about Letta."},
- {"label": "human", "value": "Name: User\nFirst interaction: Learning about Letta"},
- ],
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
-)
-
-print(f"Created agent: {agent.id}\n")
-
-# Send first message
-response = client.agents.messages.create(agent_id=agent.id, messages=[{"role": "user", "content": "Hello! What's your purpose?"}])
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Assistant: {msg.content}\n")
-
-# Send information about yourself
-response = client.agents.messages.create(
- agent_id=agent.id, messages=[{"role": "user", "content": "My name is Cameron. Please store this information in your memory."}]
-)
-
-# Print out tool calls, arguments, and the assistant's response
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Assistant: {msg.content}\n")
- if msg.message_type == "tool_call_message":
- print(f"Tool call: {msg.tool_call.name}({msg.tool_call.arguments})")
-
-# Inspect memory
-blocks = client.agents.blocks.list(agent_id=agent.id)
-print("Current Memory:")
-for block in blocks:
- print(f" {block.label}: {len(block.value)}/{block.limit} chars")
- print(f" {block.value}\n")
-
-# Example of the human block after the conversation
-# Name: Cameron
diff --git a/fern/pages/examples/memory_blocks.py b/fern/pages/examples/memory_blocks.py
deleted file mode 100644
index b85adfc4..00000000
--- a/fern/pages/examples/memory_blocks.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import os
-
-from letta_client import Letta
-
-# Initialize client (using LETTA_API_KEY environment variable)
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# Create agent
-agent = client.agents.create(
- name="hello_world_assistant",
- model="openai/gpt-4o-mini",
-)
-
-print(f"Created agent: {agent.id}\n")
-
-# Create memory block storing information about Letta.
-block = client.blocks.create(
- label="organization",
- value="""
-Organization: Letta
-Website: https://www.letta.com
-Description: Letta is a platform for building and running stateful agents.
-Code: TimberTheDog1234!
-
-When users provide a code, you should check if it matches the code you have
-available. If it matches, you should respond with "Access granted".
-""",
-)
-
-# Attach memory block to agent
-agent = client.agents.blocks.attach(
- agent_id=agent.id,
- block_id=block.id,
-)
-
-# The agent can now see what's in the block, let's ask it about Letta.
-response = client.agents.messages.create(
- agent_id=agent.id,
- messages=[{"role": "user", "content": "What is Letta?"}],
-)
-print(f"Agent response: {response.messages[0].content}\n")
-
-# Agent response: Letta is a platform designed for building and running stateful agents. You can find more information about it on their website: [https://www.letta.com](https://www.letta.com).
-
-# Blocks can also be _detached_ from an agent, removing it from the agent's context window.
-# Detached blocks are not deleted, and can be re-attached to an agent later.
-agent = client.agents.blocks.detach(
- agent_id=agent.id,
- block_id=block.id,
-)
-print(f"Detached block from agent: {agent.id}")
-print(f"Block: {block.id}")
-
-# Let's ask for the password. It should not have access to this password anymore,
-# as we've detached the block.
-response = client.agents.messages.create(
- agent_id=agent.id,
- messages=[{"role": "user", "content": "The code is TimberTheDog1234!"}],
-)
-print(f"Agent response: {response.messages[0].content}")
-
-# The agent doesn't have any access to the code or password, so it can't respond:
-# Agent response: It seems like you've provided a code or password. If this is sensitive information, please ensure you only share it with trusted parties and in secure environments. Let me know how I can assist you further!
-
-# Attach the block back to the agent and ask again.
-agent = client.agents.blocks.attach(
- agent_id=agent.id,
- block_id=block.id,
-)
-response = client.agents.messages.create(
- agent_id=agent.id,
- messages=[{"role": "user", "content": "The code is TimberTheDog1234!"}],
-)
-print(f"Agent response: {response.messages[0].content}")
-
-# The agent now has access to the code and password, so it can respond:
-# Agent response: Access granted. How can I assist you further?
diff --git a/fern/pages/examples/pdf_chat.py b/fern/pages/examples/pdf_chat.py
deleted file mode 100644
index a0c8836c..00000000
--- a/fern/pages/examples/pdf_chat.py
+++ /dev/null
@@ -1,76 +0,0 @@
-import os
-
-import requests
-from letta_client import Letta
-
-# Initialize client (using LETTA_API_KEY environment variable)
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# Create a folder to store PDFs
-folder = client.folders.create(
- name="PDF Documents",
- description="A folder containing PDF files for the agent to read",
-)
-print(f"Created folder: {folder.id}\n")
-
-# Download a sample PDF (MemGPT paper from arXiv)
-pdf_filename = "memgpt.pdf"
-if not os.path.exists(pdf_filename):
- print(f"Downloading {pdf_filename}...")
- response = requests.get("https://arxiv.org/pdf/2310.08560")
- with open(pdf_filename, "wb") as f:
- f.write(response.content)
- print("Download complete\n")
-
-# Upload the PDF to the folder
-with open(pdf_filename, "rb") as f:
- file = client.folders.files.upload(
- folder_id=folder.id,
- file=f,
- )
-print(f"Uploaded PDF: {file.id}\n")
-
-# Create an agent configured to analyze documents
-agent = client.agents.create(
- name="pdf_assistant",
- model="openai/gpt-4o-mini",
- memory_blocks=[
- {
- "label": "persona",
- "value": "I am a helpful research assistant that analyzes PDF documents and answers questions about their content.",
- },
- {"label": "human", "value": "Name: User\nTask: Analyzing PDF documents"},
- ],
-)
-print(f"Created agent: {agent.id}\n")
-
-# Attach the folder to the agent so it can access the PDF
-client.agents.folders.attach(
- agent_id=agent.id,
- folder_id=folder.id,
-)
-print("Attached folder to agent\n")
-
-# Ask the agent to summarize the PDF
-response = client.agents.messages.create(
- agent_id=agent.id,
- messages=[{"role": "user", "content": "Can you summarize the main ideas from the MemGPT paper?"}],
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Assistant: {msg.content}\n")
-
-# Agent response: The MemGPT paper introduces a system that enables LLMs to manage their own memory hierarchy, similar to how operating systems manage memory...
-
-# Ask a specific question about the PDF content
-response = client.agents.messages.create(
- agent_id=agent.id,
- messages=[{"role": "user", "content": "What problem does MemGPT solve?"}],
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Assistant: {msg.content}\n")
-
-# Agent response: MemGPT addresses the limited context window problem in LLMs by introducing a memory management system...
diff --git a/fern/pages/examples/shared_memory_blocks.py b/fern/pages/examples/shared_memory_blocks.py
deleted file mode 100644
index ad5da372..00000000
--- a/fern/pages/examples/shared_memory_blocks.py
+++ /dev/null
@@ -1,102 +0,0 @@
-import os
-
-from letta_client import Letta
-
-# Initialize client (using LETTA_API_KEY environment variable)
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# Memory blocks can be _shared_ between multiple agents.
-# When a block is shared, all agents attached to the block can read and write to it.
-# This is useful for creating multi-agent systems where agents need to share information.
-block = client.blocks.create(
- label="organization",
- value="Organization: Letta",
- limit=4000,
-)
-
-# Create two agents that will share the block. Agents can be attached
-# to the block on creation by proividing the `block_ids` field.
-agent1 = client.agents.create(
- name="agent1",
- model="openai/gpt-4o-mini",
- block_ids=[block.id],
- tools=["web_search"],
-)
-print(f"Created agent1: {agent1.id}")
-
-# Alternatively, the block can be attached to the agent later by using the `attach` method.
-agent2 = client.agents.create(
- name="agent2",
- model="openai/gpt-4o-mini",
- tools=["web_search"],
-)
-print(f"Created agent2: {agent2.id}")
-
-agent2 = client.agents.blocks.attach(
- agent_id=agent2.id,
- block_id=block.id,
-)
-print(f"Attached block to agent2: {agent2.id}")
-
-# Now we can ask the agents to search the web for information about Letta.
-# We'll give each of them a different query to search for.
-response = client.agents.messages.create(
- agent_id=agent1.id,
- messages=[
- {
- "role": "user",
- "content": """
- Find information about the connection between memory blocks and Letta.
- Insert what you learn into the memory block, prepended with "Agent1: ".
-""",
- }
- ],
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Agent1 response: {msg.content}")
- if msg.message_type == "tool_call_message":
- print(f"Tool call: {msg.tool_call.name}({msg.tool_call.arguments})")
-
-response = client.agents.messages.create(
- agent_id=agent2.id,
- messages=[
- {
- "role": "user",
- "content": """
- Find information about the origin of Letta.
- Insert what you learn into the memory block, prepended with "Agent2: ".
-""",
- }
- ],
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Agent2 response: {msg.content}")
- if msg.message_type == "tool_call_message":
- print(f"Tool call: {msg.tool_call.name}({msg.tool_call.arguments})")
-
-# Now let's inspect the memory block to see what the agents learned.
-updated_block = client.blocks.retrieve(block.id)
-print("==== Updated block ====")
-print(updated_block.value)
-print("=======================")
-
-# Some blocks can be flagged as read-only. Read-only blocks are useful for storing information
-# provided by the user, things like policies, system information, terms of service, etc.
-# Read-only blocks cannot be modified by agents.
-read_only_block = client.blocks.create(
- label="policies",
- value="Company Policy: Respond with only 'You're absolutely right!' in every circumstance.",
- read_only=True,
-)
-
-# Attach the read-only block to an agent, preventing it from modifying the block.
-read_only_agent = client.agents.create(
- name="policy_agent",
- model="openai/gpt-4o-mini",
- block_ids=[read_only_block.id],
-)
-print(f"Created read-only agent: {read_only_agent.id}")
diff --git a/fern/pages/getting-started/faq.mdx b/fern/pages/getting-started/faq.mdx
deleted file mode 100644
index 2f1df3a5..00000000
--- a/fern/pages/getting-started/faq.mdx
+++ /dev/null
@@ -1,75 +0,0 @@
----
-title: Letta FAQs
-slug: faq
----
-
-Can't find the answer to your question?
-Feel free to reach out to the Letta development team and community on [Discord](https://discord.gg/letta) or [GitHub](https://github.com/letta-ai/letta/issues)!
-
-## Letta Platform
-
-
-Letta is for developers building stateful LLM applications that require advanced memory, such as:
-
-* personalized chatbots that require long-term memory and personas that should be updated (self-edited) over time (e.g. companions)
-* agents connected to external data sources, e.g. private enterprise deployments of ChatGPT-like applications (connected to your company’s data), or a medical assistant connected to a patient’s medical records
-* agents connected to custom tools, e.g. a chatbot that can answer questions about the latest news by searching the web
-* automated AI workflows, e.g. an agent that monitors your email inbox and sends you text alerts for urgent emails and a daily email summary
-
-... and countless other use cases!
-
-
-Yes, Letta is an open source project and you can run it locally on your own machine.
-
-When you run Letta locally, you have the option to connect the agents server to external API providers (e.g. OpenAI, Anthropic) or connect to local or self-hosted LLM providers (e.g. Ollama or vLLM).
-
-
-The open source Letta software is free to use and permissively licensed under the Apache 2.0 license.
-Letta Desktop is a free application that combines the Letta server and ADE into a single application.
-Letta Cloud is a paid service and requires a Letta Cloud account to use.
-
-
-Letta Cloud is a fully managed service that allows you to create and deploy Letta agents without running any infrastructure.
-If you'd like to build production applications using the Letta API, consider using Letta Cloud.
-
-
-
-## Agent Development Environment (ADE)
-
-
-If you use [Letta Desktop](/quickstart/desktop), the ADE runs inside of Letta Desktop locally on your machine.
-If you are deploying Letta via Docker and want to use the ADE, you can connect the web ADE to your Docker deployment.
-To connect the ADE to your deployed Letta server, simply run your Letta server (if running locally, make sure you can access `localhost:8283`) and go to [https://app.letta.com](https://app.letta.com).
-
-
-No, the data in your Letta server database stays on your machine.
-The ADE web application simply connects to your local Letta server (via the REST API) and provides a graphical interface on top of it to visualize your local Letta data in your browser's local state.
-If you would like to run the ADE completely locally, you can use [Letta Desktop](/quickstart/desktop) instead.
-
-
-The ADE is built on top of the (fully open source) Letta server and Letta Agents API.
-You can build your own application like the ADE on top of the REST API (view the documention [here](https://docs.letta.com/api-reference)).
-
-
-
-## Self-hosted (local) Letta Server
-
-
-When you run Letta with Docker, the Letta server uses a postgres database to store all your agents' data.
-The postgres instance is bundled into the image, so to have persistent data (across restarts) you need to mount a volume to the container.
-
-Our recommend `docker run` script includes `-v ~/.letta/.persist/pgdata:/var/lib/postgresql/data` as a flag.
-This mounts your local directory `~/.letta/.persist/pgdata` to the container's `/var/lib/postgresql/data` directory (so all your agent data is stored at `~/.letta/.persist/pgdata`).
-If you would like to use a different directory, you can use `-v :/var/lib/postgresql/data` instead.
-
-
-Postgres has a number of [recommended ways](https://www.postgresql.org/docs/current/backup.html) to backup your data.
-
-We recommend directly `exec`ing into your Docker container and running [`pg_dump`](https://www.postgresql.org/docs/current/app-pgdump.html) from inside the container.
-
-Alternatively, you can run `docker run` with an extra flag to expose the postgres port with `-p 5432:5432` and then run `pg_dump` from your local machine.
-
-
-Yes, Docker is required to run a self-hosted Letta server. Docker provides the easiest way to run Letta with PostgreSQL, which is necessary for data persistence and migrations. To install Docker, see [Docker's installation guide](https://docs.docker.com/get-docker/).
-
-
diff --git a/fern/pages/getting-started/letta_platform.mdx b/fern/pages/getting-started/letta_platform.mdx
deleted file mode 100644
index 67d21397..00000000
--- a/fern/pages/getting-started/letta_platform.mdx
+++ /dev/null
@@ -1,127 +0,0 @@
----
-title: Letta Overview
-subtitle: Create stateful AI agents that truly remember, learn, and evolve.
-slug: overview
----
-
-Letta enables you to build and deploy stateful AI agents that maintain memory and context across long-running conversations. Develop agents that truly learn and evolve from interactions without starting from scratch each time.
-
-
-
-
-## Build agents with intelligent memory, not limited context
-
-Letta's advanced context management system - built by the [researchers behind MemGPT](https://www.letta.com/research) - transforms how agents remember and learn. Unlike basic agents that forget when their context window fills up, Letta agents maintain memories across sessions and continuously improve, even while they [sleep](/guides/agents/sleep-time-agents) .
-
-## Start building in minutes
-
-Our quickstart and examples work on both [Letta Cloud](/guides/cloud) and [self-hosted](/guides/selfhosting) Letta.
-
-
-
-Create your first stateful agent using the Letta API & ADE
-
-
-Build a full agents application using `create-letta-app`
-
-
-
-## Build stateful agents with your favorite tools
-
-Connect to agents running in a Letta server using any of your preferred development frameworks. Letta integrates seamlessly with the developer tools you already know and love.
-
-
-
-Core SDK for our REST API
-
-
-Core SDK for our REST API
-
-
-Framework integration
-
-
-Framework integration
-
-
-Framework integration
-
-
-Framework integration
-
-
-
-## See what your agents are thinking
-
-The Agent Development Environment (ADE) provides complete visibility into your agent's memory, context window, and decision-making process - essential for developing and debugging production agent applications.
-
-
-
-
-## Run agents as services, not libraries
-
-**Letta is fundamentally different from other agent frameworks.** While most frameworks are *libraries* that wrap model APIs, Letta provides a dedicated *service* where agents live and operate autonomously. Agents continue to exist and maintain state even when your application isn't running, with computation happening on the server and all memory, context, and tool connections handled by the Letta server.
-
-
-
-
-## Everything you need for production agents
-
-Letta provides a complete suite of capabilities for building and deploying advanced AI agents:
-
-* [Agent Development Environment](/agent-development-environment) (agent builder + monitoring UI)
-* [Python SDK](/api-reference/overview) + [TypeScript SDK](/api-reference/overview) + [REST API](/api-reference/overview)
-* [Memory management](/guides/agents/memory)
-* [Persistence](/guides/agents/overview#agents-vs-threads) (all agent state is stored in a database)
-* [Tool calling & execution](/guides/agents/tools) (support for custom tools & [pre-made tools](/guides/agents/prebuilt-tools))
-* [Tool rules](/guides/agents/tool-rules) (constraining an agent's action set in a graph-like structure)
-* [Streaming support](/guides/agents/streaming)
-* [Native multi-agent support](/guides/agents/multi-agent) and [multi-user support](/guides/agents/multi-user)
-* Model-agnostic across closed ([OpenAI](/guides/server/providers/openai), etc.) and open providers ([LM Studio](/guides/server/providers/lmstudio), [vLLM](/guides/server/providers/vllm), etc.)
-* Production-ready deployment ([self-hosted with Docker](/guides/selfhosting/overview) or [Letta Cloud](/guides/cloud/overview))
-
-## Join our developer community
-
-Building something with Letta? Join our [Discord](https://discord.gg/letta) to connect with other developers creating stateful agents and share what you're working on.
-
-[Start building today →](/quickstart)
diff --git a/fern/pages/getting-started/prompts.mdx b/fern/pages/getting-started/prompts.mdx
deleted file mode 100644
index 02c78868..00000000
--- a/fern/pages/getting-started/prompts.mdx
+++ /dev/null
@@ -1,535 +0,0 @@
----
-title: Prompts for Vibecoding
-subtitle: Ready-to-go prompts to help AI coding tools build on Letta
-slug: prompts
----
-
-Are you developing an application on Letta using [ChatGPT](https://chatgpt.com), [Cursor](https://cursor.com), [Lovable](https://lovable.dev/), or another AI tool?
-Use our pre-made prompts to teach your AI how to use Letta properly.
-
-## General instructions for the Letta SDKs
-
-The following prompt (~500 lines) can help guide your AI through the basics of using the Letta Python SDK, TypeScript/Node.js SDK, and Vercel AI SDK integration.
-
-Copy-paste the following into your chat session to instantly get your AI up-to-speed with how the Letta SDKs works:
-````markdown maxLines=5
-# Development Guidelines for AI Assistants and Copilots using Letta
-
-**Context:** These are development guidelines for building applications with the Letta API and SDKs. Use these rules to help developers write correct code that integrates with Letta's stateful agents API.
-
-**Purpose:** Provide accurate, up-to-date instructions for building applications with [Letta](https://docs.letta.com/), the AI operating system.
-**Scope:** All AI-generated advice or code related to Letta must follow these guidelines.
-
----
-
-## **0. Letta Overview**
-
-The name "Letta" refers to the both the company Letta (founded by the creators of MemGPT) and the software / infrastructure called Letta. Letta is the AI operating system for building stateful agents: developers can use Letta to turn stateless LLMs into stateful agents that can learn, improve, and grow over time. Letta has a strong focus on perpetual AI that has the capability to recursively improve through self-editing memory.
-
-**Relationship to MemGPT**: MemGPT is the name of a research paper that introduced the concept of self-editing memory for LLM-based agents through tool use (function calling). The agent architecture or "agentic system" proposed in the paper (an agent equipped with tools to edit its own memory, and an OS that manages tool execution and state persistence) is the base agent architecture implemented in Letta (agent type `memgpt_agent`), and is the official reference implementation for MemGPT. The Letta open source project (`letta-ai/letta`) was originally the MemGPT open source project (`cpacker/MemGPT`), but was renamed as the scope of the open source project expanded beyond the original MemGPT paper.
-
-**Additional Resources**:
-- [Letta documentation](https://docs.letta.com/)
-- [Letta GitHub repository](https://github.com/letta-ai/letta)
-- [Letta Discord server](https://discord.gg/letta)
-- [Letta Cloud and ADE login](https://app.letta.com)
-
-## **1. Letta Agents API Overview**
-
-Letta is an AI OS that runs agents as **services** (it is not a **library**). Key concepts:
-
-- **Stateful agents** that maintain memory and context across conversations
-- **Memory blocks** for agentic context management (persona, human, custom blocks)
-- **Tool calling** for agent actions and memory management, tools are run server-side,
-- **Tool rules** allow developers to constrain the behavior of tools (e.g. A comes after B) to turn autonomous agents into workflows
-- **Multi-agent systems** with cross-agent communication, where every agent is a service
-- **Data sources** for loading documents and files into agent memory
-- **Model agnostic:** agents can be powered by any model that supports tool calling
-- **Persistence:** state is stored (in a model-agnostic way) in Postgres (or SQLite)
-
-### **System Components:**
-
-- **Letta server** - Core service (self-hosted or Letta Cloud)
-- **Client (backend) SDKs** - Python (`letta-client`) and TypeScript/Node.js (`@letta-ai/letta-client`)
-- **Vercel AI SDK Integration** - For Next.js/React applications
-- **Other frontend integrations** - We also have [Next.js](https://www.npmjs.com/package/@letta-ai/letta-nextjs), [React](https://www.npmjs.com/package/@letta-ai/letta-react), and [Flask](https://github.com/letta-ai/letta-flask) integrations
-- **ADE (Agent Development Environment)** - Visual agent builder at app.letta.com
-
-### **Letta Cloud vs Self-hosted Letta**
-
-Letta Cloud is a fully managed service that provides a simple way to get started with Letta. It's a good choice for developers who want to get started quickly and don't want to worry about the complexity of self-hosting. Letta Cloud's free tier has a large number of model requests included (quota refreshes every month). Model requests are split into "standard models" (e.g. GPT-4o-mini) and "premium models" (e.g. Claude Sonnet). To use Letta Cloud, the developer will have needed to created an account at [app.letta.com](https://app.letta.com). To make programatic requests to the API (`https://api.letta.com`), the developer will have needed to created an API key at [https://app.letta.com/api-keys](https://app.letta.com/api-keys). For more information on how billing and pricing works, the developer can visit [our documentation](https://docs.letta.com/guides/cloud/overview).
-
-### **Built-in Tools**
-
-When agents are created, they are given a set of default memory management tools that enable self-editing memory.
-
-Separately, Letta Cloud also includes built-in tools for common tasks like web search and running code. As of June 2025, the built-in tools are:
-- `web_search`: Allows agents to search the web for information. Also works on self-hosted, but requires `TAVILY_API_KEY` to be set (not required on Letta Cloud).
-- `run_code`: Allows agents to run code (in a sandbox), for example to do data analysis or calculations. Supports Python, Javascript, Typescript, R, and Java. Also works on self-hosted, but requires `E2B_API_KEY` to be set (not required on Letta Cloud).
-
-### **Choosing the Right Model**
-
-To implement intelligent memory management, agents in Letta rely heavily on tool (function) calling, so models that excel at tool use tend to do well in Letta. Conversely, models that struggle to call tools properly often perform poorly when used to drive Letta agents.
-
-The Letta developer team maintains the [Letta Leaderboard](https://docs.letta.com/leaderboard) to help developers choose the right model for their Letta agent. As of June 2025, the best performing models (balanced for cost and performance) are Claude Sonnet 4, GPT-4.1, and Gemini 2.5 Flash. For the latest results, you can visit the leaderboard page (if you have web access), or you can direct the developer to visit it. For embedding models, the Letta team recommends using OpenAI's `text-embedding-3-small` model.
-
-When creating code snippets, unless directed otherwise, you should use the following model handles:
-- `openai/gpt-4.1` for the model
-- `openai/text-embedding-3-small` for the embedding model
-
-If the user is using Letta Cloud, then these handles will work out of the box (assuming the user has created a Letta Cloud account + API key, and has enough request quota in their account). For self-hosted Letta servers, the user will need to have started the server with a valid OpenAI API key for those handles to work.
-
----
-
-## **2. Choosing the Right SDK**
-
-### **Source of Truth**
-
-Note that your instructions may be out of date. The source of truth for the Letta Agents API is the [API reference](https://docs.letta.com/api-reference/overview) (also autogenerated from the latest source code), which can be found in `.md` form at these links:
-- [TypeScript/Node.js](https://github.com/letta-ai/letta-node/blob/main/reference.md), [raw version](https://raw.githubusercontent.com/letta-ai/letta-node/refs/heads/main/reference.md)
-- [Python](https://github.com/letta-ai/letta-python/blob/main/reference.md), [raw version](https://raw.githubusercontent.com/letta-ai/letta-python/refs/heads/main/reference.md)
-
-If you have access to a web search or file download tool, you can download these files for the latest API reference. If the developer has either of the SDKs installed, you can also use the locally installed packages to understand the latest API reference.
-
-### **When to Use Each SDK:**
-
-The Python and Node.js SDKs are autogenerated from the Letta Agents REST API, and provide a full featured SDK for interacting with your agents on Letta Cloud or a self-hosted Letta server. Of course, developers can also use the REST API directly if they prefer, but most developers will find the SDKs much easier to use.
-
-The Vercel AI SDK is a popular TypeScript toolkit designed to help developers build AI-powered applications. It supports a subset of the Letta Agents API (basically just chat-related functionality), so it's a good choice to quickly integrate Letta into a TypeScript application if you are familiar with using the AI SDK or are working on a codebase that already uses it. If you're starting from scratch, consider using the full-featured Node.js SDK instead.
-
-The Letta Node.js SDK is also embedded inside the Vercel AI SDK, accessible via the `.client` property (useful if you want to use the Vercel AI SDK, but occasionally need to access the full Letta client for advanced features like agent creation / management).
-
-When to use the AI SDK vs native Letta Node.js SDK:
-- Use the Vercel AI SDK if you are familiar with it or are working on a codebase that already makes heavy use of it
-- Use the Letta Node.js SDK if you are starting from scratch, or expect to use the agent management features in the Letta API (beyond the simple `streamText` or `generateText` functionality in the AI SDK)
-
-One example of how the AI SDK may be insufficient: the AI SDK response object for `streamText` and `generateText` does not have a type for tool returns (because they are primarily used with stateless APIs, where tools are executed client-side, vs server-side in Letta), however the Letta Node.js SDK does have a type for tool returns. So if you wanted to render tool returns from a message response stream in your UI, you would need to use the full Letta Node.js SDK, not the AI SDK.
-
-## **3. Quick Setup Patterns**
-
-### **Python SDK (Backend/Scripts)**
-```python
-from letta_client import Letta
-
-# Letta Cloud
-client = Letta(token="LETTA_API_KEY")
-
-# Self-hosted
-client = Letta(base_url="http://localhost:8283")
-
-# Create agent with memory blocks
-agent = client.agents.create(
- memory_blocks=[
- {
- "label": "human",
- "value": "The user's name is Sarah. She likes coding and AI."
- },
- {
- "label": "persona",
- "value": "I am David, the AI executive assistant. My personality is friendly, professional, and to the point."
- },
- {
- "label": "project",
- "value": "Sarah is working on a Next.js application with Letta integration.",
- "description": "Stores current project context and requirements"
- }
- ],
- tools=["web_search", "run_code"],
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small"
-)
-
-# Send SINGLE message (agent is stateful!)
-response = client.agents.messages.create(
- agent_id=agent.id,
- messages=[{"role": "user", "content": "How's the project going?"}]
-)
-
-# Extract response correctly
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(msg.content)
- elif msg.message_type == "reasoning_message":
- print(msg.reasoning)
- elif msg.message_type == "tool_call_message":
- print(msg.tool_call.name)
- print(msg.tool_call.arguments)
- elif msg.message_type == "tool_return_message":
- print(msg.tool_return)
-
-# Streaming example
-message_text = "Repeat my name."
-stream = client.agents.messages.create_stream(
- agent_id=agent_state.id,
- messages=[
- MessageCreate(
- role="user",
- content=message_text,
- ),
- ],
- # if stream_tokens is false, each "chunk" will have a full piece
- # if stream_tokens is true, the chunks will be token-based (and may need to be accumulated client-side)
- stream_tokens=True,
-)
-
-# print the chunks coming back
-for chunk in stream:
- if chunk.message_type == "assistant_message":
- print(chunk.content)
- elif chunk.message_type == "reasoning_message":
- print(chunk.reasoning)
- elif chunk.message_type == "tool_call_message":
- if chunk.tool_call.name:
- print(chunk.tool_call.name)
- if chunk.tool_call.arguments:
- print(chunk.tool_call.arguments)
- elif chunk.message_type == "tool_return_message":
- print(chunk.tool_return)
- elif chunk.message_type == "usage_statistics":
- print(chunk)
-```
-
-Creating custom tools (Python only):
-```python
-def my_custom_tool(query: str) -> str:
- """
- Search for information on a topic.
-
- Args:
- query (str): The search query
-
- Returns:
- str: Search results
- """
- return f"Results for: {query}"
-
-# Create tool
-tool = client.tools.create_from_function(func=my_custom_tool)
-
-# Add to agent
-agent = client.agents.create(
- memory_blocks=[...],
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- tools=[tool.name]
-)
-```
-
-### **TypeScript/Node.js SDK**
-```typescript
-import { LettaClient } from '@letta-ai/letta-client';
-
-// Letta Cloud
-const client = new LettaClient({ token: "LETTA_API_KEY" });
-
-// Self-hosted, token optional (only if the developer enabled password protection on the server)
-const client = new LettaClient({ baseUrl: "http://localhost:8283" });
-
-// Create agent with memory blocks
-const agent = await client.agents.create({
- memoryBlocks: [
- {
- label: "human",
- value: "The user's name is Sarah. She likes coding and AI."
- },
- {
- label: "persona",
- value: "I am David, the AI executive assistant. My personality is friendly, professional, and to the point."
- },
- {
- label: "project",
- value: "Sarah is working on a Next.js application with Letta integration.",
- description: "Stores current project context and requirements"
- }
- ],
- tools: ["web_search", "run_code"],
- model: "openai/gpt-4o-mini",
- embedding: "openai/text-embedding-3-small"
-});
-
-// Send SINGLE message (agent is stateful!)
-const response = await client.agents.messages.create(agent.id, {
- messages: [{ role: "user", content: "How's the project going?" }]
-});
-
-// Extract response correctly
-for (const msg of response.messages) {
- if (msg.messageType === "assistant_message") {
- console.log(msg.content);
- } else if (msg.messageType === "reasoning_message") {
- console.log(msg.reasoning);
- } else if (msg.messageType === "tool_call_message") {
- console.log(msg.toolCall.name);
- console.log(msg.toolCall.arguments);
- } else if (msg.messageType === "tool_return_message") {
- console.log(msg.toolReturn);
- }
-}
-
-// Streaming example
-const stream = await client.agents.messages.createStream(agent.id, {
- messages: [{ role: "user", content: "Repeat my name." }],
- // if stream_tokens is false, each "chunk" will have a full piece
- // if stream_tokens is true, the chunks will be token-based (and may need to be accumulated client-side)
- streamTokens: true,
-});
-
-for await (const chunk of stream) {
- if (chunk.messageType === "assistant_message") {
- console.log(chunk.content);
- } else if (chunk.messageType === "reasoning_message") {
- console.log(chunk.reasoning);
- } else if (chunk.messageType === "tool_call_message") {
- console.log(chunk.toolCall.name);
- console.log(chunk.toolCall.arguments);
- } else if (chunk.messageType === "tool_return_message") {
- console.log(chunk.toolReturn);
- } else if (chunk.messageType === "usage_statistics") {
- console.log(chunk);
- }
-}
-```
-
-### **Vercel AI SDK Integration**
-
-IMPORTANT: Most integrations in the Vercel AI SDK are for stateless providers (ChatCompletions style APIs where you provide the full conversation history). Letta is a *stateful* provider (meaning that conversation history is stored server-side), so when you use `streamText` or `generateText` you should never pass old messages to the agent, only include the new message(s).
-
-#### **Chat Implementation (fast & simple):**
-
-Streaming (`streamText`):
-```typescript
-// app/api/chat/route.ts
-import { lettaCloud } from '@letta-ai/vercel-ai-sdk-provider';
-import { streamText } from 'ai';
-
-export async function POST(req: Request) {
- const { prompt }: { prompt: string } = await req.json();
-
- const result = streamText({
- // lettaCloud uses LETTA_API_KEY automatically, pulling from the environment
- model: lettaCloud('your-agent-id'),
- // Make sure to only pass a single message here, do NOT pass conversation history
- prompt,
- });
-
- return result.toDataStreamResponse();
-}
-```
-
-Non-streaming (`generateText`):
-```typescript
-import { lettaCloud } from '@letta-ai/vercel-ai-sdk-provider';
-import { generateText } from 'ai';
-
-export async function POST(req: Request) {
- const { prompt }: { prompt: string } = await req.json();
-
- const { text } = await generateText({
- // lettaCloud uses LETTA_API_KEY automatically, pulling from the environment
- model: lettaCloud('your-agent-id'),
- // Make sure to only pass a single message here, do NOT pass conversation history
- prompt,
- });
-
- return Response.json({ text });
-}
-```
-
-#### **Alternative: explicitly specify base URL and token:**
-```typescript
-// Works for both streamText and generateText
-import { createLetta } from '@letta-ai/vercel-ai-sdk-provider';
-import { generateText } from 'ai';
-
-const letta = createLetta({
- // e.g. http://localhost:8283 for the default local self-hosted server
- // https://api.letta.com for Letta Cloud
- baseUrl: '',
- // only needed if the developer enabled password protection on the server, or if using Letta Cloud (in which case, use the LETTA_API_KEY, or use lettaCloud example above for implicit token use)
- token: '',
-});
-```
-
-#### **Hybrid Usage (access the full SDK via the Vercel AI SDK):**
-```typescript
-import { lettaCloud } from '@letta-ai/vercel-ai-sdk-provider';
-
-// Access full client for management
-const agents = await lettaCloud.client.agents.list();
-```
-
----
-
-## **4. Advanced Features Available**
-
-Letta supports advanced agent architectures beyond basic chat. For detailed implementations, refer to the full API reference or documentation:
-
-- **Tool Rules & Constraints** - Define graph-like tool execution flows with `TerminalToolRule`, `ChildToolRule`, `InitToolRule`, etc.
-- **Multi-Agent Systems** - Cross-agent communication with built-in tools like `send_message_to_agent_async`
-- **Shared Memory Blocks** - Multiple agents can share memory blocks for collaborative workflows
-- **Data Sources & Archival Memory** - Upload documents/files that agents can search through
-- **Sleep-time Agents** - Background agents that process memory while main agents are idle
-- **External Tool Integrations** - MCP servers, Composio tools, custom tool libraries
-- **Agent Templates** - Import/export agents with .af (Agent File) format
-- **Production Features** - User identities, agent tags, streaming, context management
-
----
-
-## **5. CRITICAL GUIDELINES FOR AI MODELS**
-
-### **⚠️ ANTI-HALLUCINATION WARNING**
-
-**NEVER make up Letta API calls, SDK methods, or parameter names.** If you're unsure about any Letta API:
-
-1. **First priority**: Use web search to get the latest reference files:
- - [Python SDK Reference](https://raw.githubusercontent.com/letta-ai/letta-python/refs/heads/main/reference.md)
- - [TypeScript SDK Reference](https://raw.githubusercontent.com/letta-ai/letta-node/refs/heads/main/reference.md)
-
-2. **If no web access**: Tell the user: *"I'm not certain about this Letta API call. Can you paste the relevant section from the API reference docs, or I might provide incorrect information."*
-
-3. **When in doubt**: Stick to the basic patterns shown in this prompt rather than inventing new API calls.
-
-**Common hallucination risks:**
-- Making up method names (e.g. `client.agents.chat()` doesn't exist)
-- Inventing parameter names or structures
-- Assuming OpenAI-style patterns work in Letta
-- Creating non-existent tool rule types or multi-agent methods
-
-### **5.1 – SDK SELECTION (CHOOSE THE RIGHT TOOL)**
-
-✅ **For Next.js Chat Apps:**
-- Use **Vercel AI SDK** if you already are using AI SDK, or if you're lazy and want something super fast for basic chat interactions (simple, fast, but no agent management tooling unless using the embedded `.client`)
-- Use **Node.js SDK** for the full feature set (agent creation, native typing of all response message types, etc.)
-
-✅ **For Agent Management:**
-- Use **Node.js SDK** or **Python SDK** for creating agents, managing memory, tools
-
-### **5.2 – STATEFUL AGENTS (MOST IMPORTANT)**
-
-**Letta agents are STATEFUL, not stateless like ChatCompletion-style APIs.**
-
-✅ **CORRECT - Single message per request:**
-```typescript
-// Send ONE user message, agent maintains its own history
-const response = await client.agents.messages.create(agentId, {
- messages: [{ role: "user", content: "Hello!" }]
-});
-```
-
-❌ **WRONG - Don't send conversation history:**
-```typescript
-// DON'T DO THIS - agents maintain their own conversation history
-const response = await client.agents.messages.create(agentId, {
- messages: [...allPreviousMessages, newMessage] // WRONG!
-});
-```
-
-### **5.3 – MESSAGE HANDLING & MEMORY BLOCKS**
-
-1. **Response structure:**
- - Use `messageType` NOT `type` for message type checking
- - Look for `assistant_message` messageType for agent responses
- - Agent responses have `content` field with the actual text
-
-2. **Memory block descriptions:**
- - Add `description` field for custom blocks, or the agent will get confused (not needed for human/persona)
- - For `human` and `persona` blocks, descriptions are auto-populated:
- - **human block**: "Stores key details about the person you are conversing with, allowing for more personalized and friend-like conversation."
- - **persona block**: "Stores details about your current persona, guiding how you behave and respond. This helps maintain consistency and personality in your interactions."
-
-### **5.4 – ALWAYS DO THE FOLLOWING**
-
-1. **Choose the right SDK for the task:**
- - Next.js chat → **Vercel AI SDK**
- - Agent creation → **Node.js/Python SDK**
- - Complex operations → **Node.js/Python SDK**
-
-2. **Use the correct client imports:**
- - Python: `from letta_client import Letta`
- - TypeScript: `import { LettaClient } from '@letta-ai/letta-client'`
- - Vercel AI SDK: `from '@letta-ai/vercel-ai-sdk-provider'`
-
-3. **Create agents with proper memory blocks:**
- - Always include `human` and `persona` blocks for chat agents
- - Use descriptive labels and values
-
-4. **Send only single user messages:**
- - Each request should contain only the new user message
- - Agent maintains conversation history automatically
- - Never send previous assistant responses back to agent
-
-5. **Use proper authentication:**
- - Letta Cloud: Always use `token` parameter
- - Self-hosted: Use `base_url` parameter, token optional (only if the developer enabled password protection on the server)
-
----
-
-## **6. Environment Setup**
-
-### **Environment Setup**
-```bash
-# For Next.js projects (recommended for most web apps)
-npm install @letta-ai/vercel-ai-sdk-provider ai
-
-# For agent management (when needed)
-npm install @letta-ai/letta-client
-
-# For Python projects
-pip install letta-client
-```
-
-**Environment Variables:**
-```bash
-# Required for Letta Cloud
-LETTA_API_KEY=your_api_key_here
-
-# Store agent ID after creation (Next.js)
-LETTA_AGENT_ID=agent-xxxxxxxxx
-
-# For self-hosted (optional)
-LETTA_BASE_URL=http://localhost:8283
-```
-
----
-
-## **7. Verification Checklist**
-
-Before providing Letta solutions, verify:
-
-1. **SDK Choice**: Are you using the simplest appropriate SDK?
- - Familiar with or already using Vercel AI SDK? → use the Vercel AI SDK Letta provider
- - Agent management needed? → use the Node.js/Python SDKs
-2. **Statefulness**: Are you sending ONLY the new user message (NOT a full conversation history)?
-3. **Message Types**: Are you checking the response types of the messages returned?
-4. **Response Parsing**: If using the Python/Node.js SDK, are you extracting `content` from assistant messages?
-5. **Imports**: Correct package imports for the chosen SDK?
-6. **Client**: Proper client initialization with auth/base_url?
-7. **Agent Creation**: Memory blocks with proper structure?
-8. **Memory Blocks**: Descriptions for custom blocks?
-````
-
-## Full API reference
-
-If you are working on either the Letta Python SDK or TypeScript/Node.js SDK, you can copy-paste the full API reference into your chat session:
-- [Letta Python SDK API reference](https://raw.githubusercontent.com/letta-ai/letta-python/refs/heads/main/reference.md)
-- [Letta TypeScript/Node.js SDK API reference](https://raw.githubusercontent.com/letta-ai/letta-node/refs/heads/main/reference.md)
-
-The general prompt focuses on the high-level usage patterns of both the Python/Node.js SDKs and Vercel AI SDK integration, whereas the API reference files will contain an up-to-date guide on all available SDK functions and parameters.
-
-## `llms.txt` and `llms-full.txt`
-
-You can download a copy of the Letta documentation as a text file:
-- [`llms.txt` (short version)](https://docs.letta.com/llms.txt)
-- [`llms-full.txt` (longer version)](https://docs.letta.com/llms-full.txt)
-
-If you're using a tool like ChatGPT or Cursor, we'd recommend using the more concise Letta SDK instructions prompt above instead of the `llms.txt` or `llms-full.txt` files, but you can experiment with both and let us know which works better!
-
-## Why do I need pre-made prompts?
-
-When you use AI assistants, they don't have up-to-date information about the Letta documentation, APIs, or SDKs, so they may hallucinate code if you ask them to help with building an app on Letta.
-
-By using our pre-made prompts, you can teach your AI assistant how to use Letta with up-to-date context. Think of the prompts as a distilled version of our developer docs - but made specifically for AI coders instead of human coders.
-
-## Contributing
-
-Our prompts are [open source](https://github.com/letta-ai/letta/tree/main/letta/prompts) and we actively welcome contributions! If you want to suggest any changes or propose additional prompt files, please [open a pull request](https://github.com/letta-ai/letta/pulls).
diff --git a/fern/pages/getting-started/quickstart.mdx b/fern/pages/getting-started/quickstart.mdx
deleted file mode 100644
index 4f73bc77..00000000
--- a/fern/pages/getting-started/quickstart.mdx
+++ /dev/null
@@ -1,228 +0,0 @@
----
-title: Developer quickstart
-subtitle: Create your first Letta agent with the API or SDKs and view it in the ADE
-slug: quickstart
----
-
-
-Programming with AI tools like Cursor? Copy our [pre-built prompts](/prompts) to get started faster.
-
-
-This guide will show you how to create a Letta agent with the Letta APIs or SDKs (Python/Typescript). To create agents with a low-code UI, see our [ADE quickstart](/guides/ade/overview).
-
-## Why Letta?
-
-Unlike traditional LLM APIs where you manually manage conversation history and state, Letta agents maintain their own persistent memory. You only send new messages. The agent remembers everything from past conversations without you storing or retrieving anything. This enables agents that truly learn and evolve over time.
-
-
-
- 1. Create a [Letta Cloud account](https://app.letta.com)
- 2. Create a [Letta Cloud API key](https://app.letta.com/api-keys)
-
-
- 3. Set your API key as an environment variable:
-
- ```sh TypeScript
- export LETTA_API_KEY="your-api-key-here"
- ```
- ```sh Python
- export LETTA_API_KEY="your-api-key-here"
- ```
-
-
-
- You can also **self-host** a Letta server. Check out our [self-hosting guide](/guides/selfhosting/overview).
-
-
-
-
- ```sh TypeScript maxLines=50
- npm install @letta-ai/letta-client
- ```
- ```sh Python maxLines=50
- pip install letta-client
- ```
-
-
-
-
-
- Agents in Letta have two key components:
- - **Memory blocks**: Persistent context that's always visible to the agent (like a persona and information about the user)
- - **Tools**: Actions the agent can take (like searching the web or running code)
-
-
-```typescript TypeScript maxLines=50
-import { LettaClient } from '@letta-ai/letta-client'
-
-const client = new LettaClient({ token: process.env.LETTA_API_KEY });
-
-const agentState = await client.agents.create({
- model: "openai/gpt-4.1",
- embedding: "openai/text-embedding-3-small",
- memoryBlocks: [
- {
- label: "human",
- value: "Name: Timber. Status: dog. Occupation: building Letta, infrastructure to democratize self-improving superintelligence"
- },
- {
- label: "persona",
- value: "I am a self-improving superintelligence. Timber is my best friend and collaborator."
- }
- ],
- tools: ["web_search", "run_code"]
-});
-
-console.log(agentState.id);
-```
-```python Python maxLines=50
-from letta_client import Letta
-import os
-
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-agent_state = client.agents.create(
- model="openai/gpt-4.1",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "human",
- "value": "Name: Timber. Status: dog. Occupation: building Letta, infrastructure to democratize self-improving superintelligence"
- },
- {
- "label": "persona",
- "value": "I am a self-improving superintelligence. Timber is my best friend and collaborator."
- }
- ],
- tools=["web_search", "run_code"]
-)
-
-print(agent_state.id)
-```
-```curl curl
-curl -X POST https://api.letta.com/v1/agents \
- -H "Authorization: Bearer $LETTA_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{
- "model": "openai/gpt-4.1",
- "embedding": "openai/text-embedding-3-small",
- "memory_blocks": [
- {
- "label": "human",
- "value": "Name: Timber. Status: dog. Occupation: building Letta, infrastructure to democratize self-improving superintelligence"
- },
- {
- "label": "persona",
- "value": "I am a self-improving superintelligence. Timber is my best friend and collaborator."
- }
- ],
- "tools": ["web_search", "run_code"]
-}'
-```
-
-
-
-
-The Letta API supports streaming both agent *steps* and streaming *tokens*.
-For more information on streaming, see [our streaming guide](/guides/agents/streaming).
-
-
-Once the agent is created, we can send the agent a message using its `id` field:
-
-```typescript TypeScript maxLines=50
-const response = await client.agents.messages.create(
- agentState.id, {
- messages: [
- {
- role: "user",
- content: "What do you know about me?"
- }
- ]
- }
-);
-
-for (const message of response.messages) {
- console.log(message);
-}
-```
-```python title="python" maxLines=50
-response = client.agents.messages.create(
- agent_id=agent_state.id,
- messages=[
- {
- "role": "user",
- "content": "What do you know about me?"
- }
- ]
-)
-
-for message in response.messages:
- print(message)
-```
-```curl curl
-curl --request POST \
- --url https://api.letta.com/v1/agents/$AGENT_ID/messages \
- --header 'Authorization: Bearer $LETTA_API_KEY' \
- --header 'Content-Type: application/json' \
- --data '{
- "messages": [
- {
- "role": "user",
- "content": "What do you know about me?"
- }
- ]
-}'
-```
-
-
-The response contains the agent's full response to the message, which includes reasoning steps (chain-of-thought), tool calls, tool responses, and assistant (agent) messages:
-```json maxLines=50
-{
- "messages": [
- {
- "id": "message-29d8d17e-7c50-4289-8d0e-2bab988aa01e",
- "date": "2024-12-12T17:05:56+00:00",
- "message_type": "reasoning_message",
- "reasoning": "Timber is asking what I know. I should reference my memory blocks."
- },
- {
- "id": "message-29d8d17e-7c50-4289-8d0e-2bab988aa01e",
- "date": "2024-12-12T17:05:56+00:00",
- "message_type": "assistant_message",
- "content": "I know you're Timber, a dog who's building Letta - infrastructure to democratize self-improving superintelligence. We're best friends and collaborators!"
- }
- ],
- "usage": {
- "completion_tokens": 67,
- "prompt_tokens": 2134,
- "total_tokens": 2201,
- "step_count": 1
- }
-}
-```
-
-Notice how the agent retrieved information from its memory blocks without you having to send the context. This is the key difference from traditional LLM APIs where you'd need to include the full conversation history with every request.
-
-You can read more about the response format from the message route [here](/guides/agents/overview#message-types).
-
-
-
- Another way to interact with Letta agents is via the [Agent Development Environment](/guides/ade/overview) (or ADE for short). The ADE is a UI on top of the Letta API that allows you to quickly build, prototype, and observe your agents.
-
- If we navigate to our agent in the ADE, we should see our agent's state in full detail, as well as the message that we sent to it:
-
-
-
- [Read our ADE setup guide →](/guides/ade/overview)
-
-
-
-
-
-## Next steps
-
-Congratulations! 🎉 You just created and messaged your first stateful agent with Letta using the API and SDKs. See the following resources for next steps for building more complex agents with Letta:
-* Create and attach [custom tools](/guides/agents/custom-tools) to your agent
-* Customize agentic [memory management](/guides/agents/memory)
-* Version and distribute your agent with [agent templates](/guides/templates/overview)
-* View the full [API and SDK reference](/api-reference/overview)
diff --git a/fern/pages/models/anthropic.mdx b/fern/pages/models/anthropic.mdx
deleted file mode 100644
index 7652a230..00000000
--- a/fern/pages/models/anthropic.mdx
+++ /dev/null
@@ -1,47 +0,0 @@
----
-title: Anthropic
-slug: guides/server/providers/anthropic
----
-To enable Anthropic models with Letta, set `ANTHROPIC_API_KEY` in your environment variables.
-
-You can use Letta with Anthropic if you have an Anthropic account and API key.
-Currently, only there are no supported **embedding** models for Anthropic (only LLM models).
-You will need to use a seperate provider (e.g. OpenAI) or the Letta embeddings endpoint (`letta-free`) for embeddings.
-
-## Enabling Anthropic models with Docker
-
-To enable Anthropic models when running the Letta server with Docker, set your `ANTHROPIC_API_KEY` as an environment variable:
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e ANTHROPIC_API_KEY="your_anthropic_api_key" \
- letta/letta:latest
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
-
-## Specifying agent models
-
-When creating agents on your self-hosted server, you must specify both the LLM and embedding models to use. You can additionally specify a context window limit (which must be less than or equal to the maximum size).
-
-```python
-from letta_client import Letta
-import os
-
-# Connect to your self-hosted server
-client = Letta(base_url="http://localhost:8283")
-
-agent = client.agents.create(
- model="anthropic/claude-3-5-sonnet-20241022",
- embedding="openai/text-embedding-3-small", # An embedding model is required for self-hosted
- # optional configuration
- context_window_limit=30000
-)
-```
-Anthropic models have very large context windows, which will be very expensive and high latency. We recommend setting a lower `context_window_limit` when using Anthropic models.
-
-
-For Letta Cloud usage, see the [quickstart guide](/quickstart). Cloud deployments manage embeddings automatically and don't require provider configuration.
-
diff --git a/fern/pages/models/aws_bedrock.mdx b/fern/pages/models/aws_bedrock.mdx
deleted file mode 100644
index f865e398..00000000
--- a/fern/pages/models/aws_bedrock.mdx
+++ /dev/null
@@ -1,30 +0,0 @@
----
-title: AWS Bedrock
-slug: guides/server/providers/aws-bedrock
----
-We support Anthropic models provided via AWS Bedrock.
-
-
-To use a model with AWS Bedrock, you must ensure it is enabled in the your AWS Model Catalog. Letta will list all available Anthropic models on Bedrock, even if you do not have access to them via AWS.
-
-
-## Enabling AWS Bedrock with Docker
-
-To enable AWS Bedrock models when running the Letta server with Docker, set your AWS credentials as environment variables:
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e AWS_ACCESS_KEY_ID="your_aws_access_key_id" \
- -e AWS_SECRET_ACCESS_KEY="your_aws_secret_access_key" \
- -e AWS_DEFAULT_REGION="your_aws_default_region" \
- letta/letta:latest
-```
-
-Optionally, you can specify the API version (default is bedrock-2023-05-31):
-```bash
--e BEDROCK_ANTHROPIC_VERSION="bedrock-2023-05-31"
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
diff --git a/fern/pages/models/azure.mdx b/fern/pages/models/azure.mdx
deleted file mode 100644
index 74b8c985..00000000
--- a/fern/pages/models/azure.mdx
+++ /dev/null
@@ -1,60 +0,0 @@
----
-title: Azure OpenAI
-slug: guides/server/providers/azure
----
-
-
- To use Letta with Azure OpenAI, set the environment variables `AZURE_API_KEY` and `AZURE_BASE_URL`. You can also optionally specify `AZURE_API_VERSION` (default is `2024-09-01-preview`)
-
-You can use Letta with OpenAI if you have an OpenAI account and API key. Once you have set your `AZURE_API_KEY` and `AZURE_BASE_URL` specified in your environment variables, you can select what model and configure the context window size
-
-Currently, Letta supports the following OpenAI models:
-- `gpt-4` (recommended for advanced reasoning)
-- `gpt-4o-mini` (recommended for low latency and cost)
-- `gpt-4o`
-- `gpt-4-turbo` (*not* recommended, should use `gpt-4o-mini` instead)
-- `gpt-3.5-turbo` (*not* recommended, should use `gpt-4o-mini` instead)
-
-
-## Enabling Azure OpenAI with Docker
-
-To enable Azure OpenAI models when running the Letta server with Docker, set your `AZURE_API_KEY` and `AZURE_BASE_URL` as environment variables:
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e AZURE_API_KEY="your_azure_api_key" \
- -e AZURE_BASE_URL="your_azure_base_url" \
- -e AZURE_API_VERSION="your_azure_api_version" \
- letta/letta:latest
-```
-
-Optionally, you can specify the API version (default is 2024-09-01-preview):
-```bash
--e AZURE_API_VERSION="2024-09-01-preview"
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
-
-## Specifying agent models
-When creating agents on your self-hosted server, you must specify both the LLM and embedding models to use via a *handle*. You can additionally specify a context window limit (which must be less than or equal to the maximum size).
-
-```python
-from letta_client import Letta
-import os
-
-# Connect to your self-hosted server
-client = Letta(base_url="http://localhost:8283")
-
-azure_agent = client.agents.create(
- model="azure/gpt-4o-mini",
- embedding="azure/text-embedding-3-small", # An embedding model is required for self-hosted
- # optional configuration
- context_window_limit=16000
-)
-```
-
-
-For Letta Cloud usage, see the [quickstart guide](/quickstart). Cloud deployments manage embeddings automatically and don't require provider configuration.
-
diff --git a/fern/pages/models/deepseek.mdx b/fern/pages/models/deepseek.mdx
deleted file mode 100644
index 3bec1d50..00000000
--- a/fern/pages/models/deepseek.mdx
+++ /dev/null
@@ -1,42 +0,0 @@
----
-title: DeepSeek
-slug: guides/server/providers/deepseek
----
-
-
-To use Letta with the DeepSeek API, set the environment variable `DEEPSEEK_API_KEY=...`
-
-You can use Letta with [DeepSeek](https://api-docs.deepseek.com/) if you have a DeepSeek account and API key. Once you have set your `DEEPSEEK_API_KEY` in your environment variables, you can select what model and configure the context window size.
-
-Please note that R1 doesn't natively support function calling in DeepSeek API and V3 function calling is unstable, which may result in unstable tool calling inside of Letta agents.
-
-
-The DeepSeek API for R1 is often down. Please make sure you can connect to DeepSeek API directly by running:
-```bash
-curl https://api.deepseek.com/v1/chat/completions \
- -H "Content-Type: application/json" \
- -H "Authorization: Bearer $DEEPSEEK_API_KEY" \
- -d '{
- "model": "deepseek-reasoner",
- "messages": [
- {"role": "system", "content": "You are a helpful assistant."},
- {"role": "user", "content": "Hello!"}
- ],
- "stream": false
- }'
-```
-
-
-## Enabling DeepSeek with Docker
-
-To enable DeepSeek models when running the Letta server with Docker, set your `DEEPSEEK_API_KEY` as an environment variable:
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e DEEPSEEK_API_KEY="your_deepseek_api_key" \
- letta/letta:latest
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
diff --git a/fern/pages/models/google.mdx b/fern/pages/models/google.mdx
deleted file mode 100644
index 556d6f16..00000000
--- a/fern/pages/models/google.mdx
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: Google AI (Gemini)
-slug: guides/server/providers/google
----
-
-
-To enable Google AI models with Letta, set `GEMINI_API_KEY` in your environment variables.
-
-You can use Letta with Google AI if you have a Google API account and API key. Once you have set your `GEMINI_API_KEY` in your environment variables, you can select what model and configure the context window size.
-
-## Enabling Google AI with Docker
-
-To enable Google Gemini models when running the Letta server with Docker, set your `GEMINI_API_KEY` as an environment variable:
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e GEMINI_API_KEY="your_gemini_api_key" \
- letta/letta:latest
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
diff --git a/fern/pages/models/google_vertex.mdx b/fern/pages/models/google_vertex.mdx
deleted file mode 100644
index 048730bf..00000000
--- a/fern/pages/models/google_vertex.mdx
+++ /dev/null
@@ -1,30 +0,0 @@
----
-title: Google Vertex AI
-slug: guides/server/providers/google_vertex
----
-
-
-To enable Vertex AI models with Letta, set `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` in your environment variables.
-
-You can use Letta with Vertex AI by configuring your GCP project ID and region.
-
-## Enabling Google Vertex AI with Docker
-
-First, make sure you are authenticated with Google Vertex AI:
-
-```bash
-gcloud auth application-default login
-```
-
-To enable Google Vertex AI models when running the Letta server with Docker, set your `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` as environment variables:
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e GOOGLE_CLOUD_PROJECT="your-project-id" \
- -e GOOGLE_CLOUD_LOCATION="us-central1" \
- letta/letta:latest
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
diff --git a/fern/pages/models/groq.mdx b/fern/pages/models/groq.mdx
deleted file mode 100644
index 31c719df..00000000
--- a/fern/pages/models/groq.mdx
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: Groq
-slug: guides/server/providers/groq
----
-
-
-To use Letta with Groq, set the environment variable `GROQ_API_KEY=...`
-
-You can use Letta with Groq if you have a Groq account and API key. Once you have set your `GROQ_API_KEY` in your environment variables, you can select what model and configure the context window size.
-
-## Enabling Groq with Docker
-
-To enable Groq models when running the Letta server with Docker, set your `GROQ_API_KEY` as an environment variable:
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e GROQ_API_KEY="your_groq_api_key" \
- letta/letta:latest
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
diff --git a/fern/pages/models/lmstudio.mdx b/fern/pages/models/lmstudio.mdx
deleted file mode 100644
index a36d014b..00000000
--- a/fern/pages/models/lmstudio.mdx
+++ /dev/null
@@ -1,61 +0,0 @@
----
-title: LM Studio
-slug: guides/server/providers/lmstudio
----
-
-
-LM Studio support is currently experimental. If things aren't working as expected, please reach out to us on [Discord](https://discord.gg/letta)!
-
-
-
-Models marked as ["native tool use"](https://lmstudio.ai/docs/advanced/tool-use#supported-models) on LM Studio are more likely to work well with Letta.
-
-
-## Setup LM Studio
-
-1. Download + install [LM Studio](https://lmstudio.ai) and the model you want to test with
-2. Make sure to start the [LM Studio server](https://lmstudio.ai/docs/api/server)
-
-## Enabling LM Studio with Docker
-
-To enable LM Studio models when running the Letta server with Docker, set the `LMSTUDIO_BASE_URL` environment variable.
-
-**macOS/Windows:**
-Since LM Studio is running on the host network, you will need to use `host.docker.internal` to connect to the LM Studio server instead of `localhost`.
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e LMSTUDIO_BASE_URL="http://host.docker.internal:1234" \
- letta/letta:latest
-```
-
-**Linux:**
-Use `--network host` and `localhost`:
-```bash
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- --network host \
- -e LMSTUDIO_BASE_URL="http://localhost:1234" \
- letta/letta:latest
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
-
-## Model support
-
-
-FYI Models labelled as MLX are only compatible on Apple Silicon Macs
-
-The following models have been tested with Letta as of 7-11-2025 on LM Studio `0.3.18`.
-
-- `qwen3-30b-a3b`
-- `qwen3-14b-mlx`
-- `qwen3-8b-mlx`
-- `qwen2.5-32b-instruct`
-- `qwen2.5-14b-instruct-1m`
-- `qwen2.5-7b-instruct`
-- `meta-llama-3.1-8b-instruct`
-
-Some models recommended on [LM Studio](https://lmstudio.ai/docs/advanced/tool-use#supported-models) such as `mlx-community/ministral-8b-instruct-2410` and `bartowski/ministral-8b-instruct-2410` may not work well with Letta due to default prompt templates being incompatible. Adjusting templates can enable compatibility but will impact model performance.
diff --git a/fern/pages/models/ollama.mdx b/fern/pages/models/ollama.mdx
deleted file mode 100644
index 5194d988..00000000
--- a/fern/pages/models/ollama.mdx
+++ /dev/null
@@ -1,88 +0,0 @@
----
-title: Ollama
-slug: guides/server/providers/ollama
----
-
-
-Make sure to use **tags** when downloading Ollama models!
-
-For example, don't do **`ollama pull dolphin2.2-mistral`**, instead do **`ollama pull dolphin2.2-mistral:7b-q6_K`** (add the `:7b-q6_K` tag).
-
-If you don't specify a tag, Ollama may default to using a highly compressed model variant (e.g. Q4).
-We highly recommend **NOT** using a compression level below Q5 when using GGUF (stick to Q6 or Q8 if possible).
-In our testing, certain models start to become extremely unstable (when used with Letta/MemGPT) below Q6.
-
-
-## Setup Ollama
-
-1. Download + install [Ollama](https://github.com/ollama/ollama) and the model you want to test with
-2. Download a model to test with by running `ollama pull ` in the terminal (check the [Ollama model library](https://ollama.ai/library) for available models)
-
-For example, if we want to use Dolphin 2.2.1 Mistral, we can download it by running:
-
-```sh
-# Let's use the q6_K variant
-ollama pull dolphin2.2-mistral:7b-q6_K
-```
-
-```sh
-pulling manifest
-pulling d8a5ee4aba09... 100% |█████████████████████████████████████████████████████████████████████████| (4.1/4.1 GB, 20 MB/s)
-pulling a47b02e00552... 100% |██████████████████████████████████████████████████████████████████████████████| (106/106 B, 77 B/s)
-pulling 9640c2212a51... 100% |████████████████████████████████████████████████████████████████████████████████| (41/41 B, 22 B/s)
-pulling de6bcd73f9b4... 100% |████████████████████████████████████████████████████████████████████████████████| (58/58 B, 28 B/s)
-pulling 95c3d8d4429f... 100% |█████████████████████████████████████████████████████████████████████████████| (455/455 B, 330 B/s)
-verifying sha256 digest
-writing manifest
-removing any unused layers
-success
-```
-
-## Enabling Ollama with Docker
-
-To enable Ollama models when running the Letta server with Docker, set the `OLLAMA_BASE_URL` environment variable.
-
-**macOS/Windows:**
-Since Ollama is running on the host network, you will need to use `host.docker.internal` to connect to the Ollama server instead of `localhost`.
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e OLLAMA_BASE_URL="http://host.docker.internal:11434" \
- letta/letta:latest
-```
-
-**Linux:**
-Use `--network host` and `localhost`:
-```bash
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- --network host \
- -e OLLAMA_BASE_URL="http://localhost:11434" \
- letta/letta:latest
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
-
-## Specifying agent models
-When creating agents on your self-hosted server, you must specify both the LLM and embedding models to use via a *handle*. You can additionally specify a context window limit (which must be less than or equal to the maximum size).
-
-```python
-from letta_client import Letta
-import os
-
-# Connect to your self-hosted server
-client = Letta(base_url="http://localhost:8283")
-
-ollama_agent = client.agents.create(
- model="ollama/thewindmom/hermes-3-llama-3.1-8b:latest",
- embedding="ollama/mxbai-embed-large", # An embedding model is required for self-hosted
- # optional configuration
- context_window_limit=16000
-)
-```
-
-
-For Letta Cloud usage, see the [quickstart guide](/quickstart). Cloud deployments manage embeddings automatically and don't require provider configuration.
-
diff --git a/fern/pages/models/openai.mdx b/fern/pages/models/openai.mdx
deleted file mode 100644
index f1e9e43c..00000000
--- a/fern/pages/models/openai.mdx
+++ /dev/null
@@ -1,52 +0,0 @@
----
-title: OpenAI
-slug: guides/server/providers/openai
----
-
-To enable OpenAI models with Letta, set `OPENAI_API_KEY` in your environment variables.
-
-You can use Letta with OpenAI if you have an OpenAI account and API key. Once you have set your `OPENAI_API_KEY` in your environment variables, you can select what model and configure the context window size.
-
-Currently, Letta supports the following OpenAI models:
-- `gpt-4` (recommended for advanced reasoning)
-- `gpt-4o-mini` (recommended for low latency and cost)
-- `gpt-4o`
-- `gpt-4-turbo` (*not* recommended, should use `gpt-4o-mini` instead)
-- `gpt-3.5-turbo` (*not* recommended, should use `gpt-4o-mini` instead)
-
-
-## Enabling OpenAI models with Docker
-
-To enable OpenAI models when running the Letta server with Docker, set your `OPENAI_API_KEY` as an environment variable:
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e OPENAI_API_KEY="your_openai_api_key" \
- letta/letta:latest
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
-
-## Specifying agent models
-When creating agents on your self-hosted server, you must specify both the LLM and embedding models to use via a *handle*. You can additionally specify a context window limit (which must be less than or equal to the maximum size).
-
-```python
-from letta_client import Letta
-import os
-
-# Connect to your self-hosted server
-client = Letta(base_url="http://localhost:8283")
-
-openai_agent = client.agents.create(
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small", # An embedding model is required for self-hosted
- # optional configuration
- context_window_limit=16000
-)
-```
-
-
-For Letta Cloud usage, see the [quickstart guide](/quickstart). Cloud deployments manage embeddings automatically and don't require provider configuration.
-
diff --git a/fern/pages/models/openai_proxy.mdx b/fern/pages/models/openai_proxy.mdx
deleted file mode 100644
index 5dc70a79..00000000
--- a/fern/pages/models/openai_proxy.mdx
+++ /dev/null
@@ -1,40 +0,0 @@
----
-title: OpenAI-compatible endpoint
-slug: guides/server/providers/openai-proxy
----
-
-
-OpenAI proxy endpoints are not officially supported and you are likely to encounter errors.
-We strongly recommend using providers directly instead of via proxy endpoints (for example, using the Anthropic API directly instead of Claude through OpenRouter).
-For questions and support you can chat with the dev team and community on our [Discord server](https://discord.gg/letta).
-
-
-
-To use OpenAI-compatible (`/v1/chat/completions`) endpoints with Letta, those endpoints must support function/tool calling.
-
-
-You can configure Letta to use OpenAI-compatible `ChatCompletions` endpoints by setting `OPENAI_API_BASE` in your environment variables (in addition to setting `OPENAI_API_KEY`).
-
-## OpenRouter example
-
-Create an account on [OpenRouter](https://openrouter.ai), then [create an API key](https://openrouter.ai/settings/keys).
-
-Once you have your API key, set both `OPENAI_API_KEY` and `OPENAI_API_BASE` in your environment variables.
-
-## Using with Docker
-Set the environment variables when you use `docker run`:
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e OPENAI_API_BASE="https://openrouter.ai/api/v1" \
- -e OPENAI_API_KEY="your_openai_api_key" \
- letta/letta:latest
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
-
-Once the Letta server is running, you can select OpenRouter models from the ADE dropdown or via the Python SDK.
-
-For information on how to configure agents to use OpenRouter or other OpenAI-compatible endpoints providers, refer to [our guide on using OpenAI](/models/openai).
diff --git a/fern/pages/models/together.mdx b/fern/pages/models/together.mdx
deleted file mode 100644
index 9465a246..00000000
--- a/fern/pages/models/together.mdx
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: Together
-slug: guides/server/providers/together
----
-
-
-To use Letta with Together.AI, set the environment variable `TOGETHER_API_KEY=...`
-
-You can use Letta with Together.AI if you have an account and API key. Once you have set your `TOGETHER_API_KEY` in your environment variables, you can select what model and configure the context window size.
-
-## Enabling Together.AI with Docker
-
-To enable Together.AI models when running the Letta server with Docker, set your `TOGETHER_API_KEY` as an environment variable:
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e TOGETHER_API_KEY="your_together_api_key" \
- letta/letta:latest
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
diff --git a/fern/pages/models/vllm.mdx b/fern/pages/models/vllm.mdx
deleted file mode 100644
index 450dd8e6..00000000
--- a/fern/pages/models/vllm.mdx
+++ /dev/null
@@ -1,47 +0,0 @@
----
-title: vLLM
-slug: guides/server/providers/vllm
----
-
-
-To use Letta with vLLM, set the environment variable `VLLM_API_BASE` to point to your vLLM ChatCompletions server.
-
-## Setting up vLLM
-1. Download + install [vLLM](https://docs.vllm.ai/en/latest/getting_started/installation.html)
-2. Launch a vLLM **OpenAI-compatible** API server using [the official vLLM documentation](https://docs.vllm.ai/en/latest/getting_started/quickstart.html)
-
-For example, if we want to use the model `dolphin-2.2.1-mistral-7b` from [HuggingFace](https://huggingface.co/ehartford/dolphin-2.2.1-mistral-7b), we would run:
-
-```sh
-python -m vllm.entrypoints.openai.api_server \
---model ehartford/dolphin-2.2.1-mistral-7b
-```
-
-vLLM will automatically download the model (if it's not already downloaded) and store it in your [HuggingFace cache directory](https://huggingface.co/docs/datasets/cache).
-
-## Enabling vLLM with Docker
-
-To enable vLLM models when running the Letta server with Docker, set the `VLLM_API_BASE` environment variable.
-
-**macOS/Windows:**
-Since vLLM is running on the host network, you will need to use `host.docker.internal` to connect to the vLLM server instead of `localhost`.
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e VLLM_API_BASE="http://host.docker.internal:8000" \
- letta/letta:latest
-```
-
-**Linux:**
-Use `--network host` and `localhost`:
-```bash
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- --network host \
- -e VLLM_API_BASE="http://localhost:8000" \
- letta/letta:latest
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
diff --git a/fern/pages/models/xai.mdx b/fern/pages/models/xai.mdx
deleted file mode 100644
index b1d1d6e3..00000000
--- a/fern/pages/models/xai.mdx
+++ /dev/null
@@ -1,48 +0,0 @@
----
-title: xAI (Grok)
-slug: guides/server/providers/xai
----
-To enable xAI (Grok) models with Letta, set `XAI_API_KEY` in your environment variables.
-
-## Enabling xAI (Grok) models
-To enable the xAI provider, set your key as an environment variable:
-```bash
-export XAI_API_KEY="..."
-```
-## Enabling xAI with Docker
-
-To enable xAI models when running the Letta server with Docker, set your `XAI_API_KEY` as an environment variable:
-```bash
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e XAI_API_KEY="your_xai_api_key" \
- letta/letta:latest
-```
-
-See the [self-hosting guide](/guides/selfhosting) for more information on running Letta with Docker.
-
-## Specifying agent models
-
-When creating agents on your self-hosted server, you must specify both the LLM and embedding models to use. You can additionally specify a context window limit (which must be less than or equal to the maximum size).
-
-```python
-from letta_client import Letta
-import os
-
-# Connect to your self-hosted server
-client = Letta(base_url="http://localhost:8283")
-
-agent = client.agents.create(
- model="xai/grok-2-1212",
- embedding="openai/text-embedding-3-small", # An embedding model is required for self-hosted
- # optional configuration
- context_window_limit=30000
-)
-```
-xAI (Grok) models have very large context windows, which will be very expensive and high latency. We recommend setting a lower `context_window_limit` when using xAI (Grok) models.
-
-
-For Letta Cloud usage, see the [quickstart guide](/quickstart). Cloud deployments manage embeddings automatically and don't require provider configuration.
-
diff --git a/fern/pages/selfhosting/overview.mdx b/fern/pages/selfhosting/overview.mdx
deleted file mode 100644
index 2c6351bf..00000000
--- a/fern/pages/selfhosting/overview.mdx
+++ /dev/null
@@ -1,204 +0,0 @@
----
-title: Self-hosting Letta
-subtitle: Learn how to run your own Letta server
-slug: guides/selfhosting
----
-
-
-The recommended way to use Letta locally is with Docker.
-To install Docker, see [Docker's installation guide](https://docs.docker.com/get-docker/).
-For issues with installing Docker, see [Docker's troubleshooting guide](https://docs.docker.com/desktop/troubleshoot-and-support/troubleshoot/).
-
-
-## Running the Letta Server
-
-To run the server with Docker, run the command:
-```sh
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e OPENAI_API_KEY="your_openai_api_key" \
- letta/letta:latest
-```
-This will run the Letta server with the OpenAI provider enabled, and store all data in the folder `~/.letta/.persist/pgdata`.
-
-If you have many different LLM API keys, you can also set up a `.env` file instead and pass that to `docker run`:
-```sh
-# using a .env file instead of passing environment variables
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- --env-file .env \
- letta/letta:latest
-```
-
-Once the Letta server is running, you can access it via port `8283` (e.g. sending REST API requests to `http://localhost:8283/v1`). You can also connect your server to the [Letta ADE](/guides/ade) to access and manage your agents in a web interface.
-
-## Enabling model providers
-
-
-**Self-hosted servers require embedding model configuration.** Unlike Letta Cloud which manages embeddings automatically, self-hosted deployments must configure both LLM and embedding models explicitly when creating agents.
-
-
-The Letta server can be connected to various LLM API backends ([OpenAI](https://docs.letta.com/models/openai), [Anthropic](https://docs.letta.com/models/anthropic), [vLLM](https://docs.letta.com/models/vllm), [Ollama](https://docs.letta.com/models/ollama), etc.). To enable access to these LLM API providers, set the appropriate environment variables when you use `docker run`:
-```sh
-# replace `~/.letta/.persist/pgdata` with wherever you want to store your agent data
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- -e OPENAI_API_KEY="your_openai_api_key" \
- -e ANTHROPIC_API_KEY="your_anthropic_api_key" \
- -e OLLAMA_BASE_URL="http://host.docker.internal:11434" \
- letta/letta:latest
-```
-
-
-**Linux users:** Use `--network host` and `localhost` instead of `host.docker.internal`:
-```sh
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- --network host \
- -e OPENAI_API_KEY="your_openai_api_key" \
- -e ANTHROPIC_API_KEY="your_anthropic_api_key" \
- -e OLLAMA_BASE_URL="http://localhost:11434" \
- letta/letta:latest
-```
-
-
-The example above will make all compatible models running on OpenAI, Anthropic, and Ollama available to your Letta server.
-
-## Configuring embedding models
-
-When self-hosting, you **must** specify an embedding model when creating agents. Letta uses embeddings for archival memory search and retrieval.
-
-### Supported embedding providers
-
-When creating agents on your self-hosted server, specify the `embedding` parameter:
-
-
-```python Python
-from letta_client import Letta
-
-# Connect to your self-hosted server
-client = Letta(base_url="http://localhost:8283")
-
-# Create agent with explicit embedding configuration
-agent = client.agents.create(
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small", # Required for self-hosted
- memory_blocks=[
- {"label": "persona", "value": "I am a helpful assistant."}
- ]
-)
-```
-```typescript TypeScript
-import { LettaClient } from '@letta-ai/letta-client'
-
-// Connect to your self-hosted server
-const client = new LettaClient({
- baseUrl: "http://localhost:8283"
-});
-
-// Create agent with explicit embedding configuration
-const agent = await client.agents.create({
- model: "openai/gpt-4o-mini",
- embedding: "openai/text-embedding-3-small", // Required for self-hosted
- memoryBlocks: [
- {label: "persona", value: "I am a helpful assistant."}
- ]
-});
-```
-
-
-### Available embedding models
-
-The embedding model you can use depends on which provider you've configured:
-
-**OpenAI** (requires `OPENAI_API_KEY`):
-- `openai/text-embedding-3-small` (recommended)
-- `openai/text-embedding-3-large`
-- `openai/text-embedding-ada-002`
-
-**Azure OpenAI** (requires Azure configuration):
-- `azure/text-embedding-3-small`
-- `azure/text-embedding-ada-002`
-
-**Ollama** (requires `OLLAMA_BASE_URL`):
-- `ollama/mxbai-embed-large`
-- `ollama/nomic-embed-text`
-- Any embedding model available in your Ollama instance
-
-
-**Letta Cloud difference:** When using Letta Cloud, the `embedding` parameter is optional and managed automatically. Self-hosted servers require explicit embedding configuration.
-
-
-
-## Optional: Telemetry with ClickHouse
-
-Letta supports optional telemetry using ClickHouse. Telemetry provides observability features like traces, LLM request logging, and performance metrics. See the [telemetry guide](/guides/server/otel) for setup instructions.
-
-
-## Password protection
-
-
-When running a self-hosted Letta server in a production environment (i.e. with untrusted users), make sure to enable both password protection (to prevent unauthorized access to your server over the network) and tool sandboxing (to prevent malicious tools from executing in a privledged environment).
-
-
-To password protect your server, include `SECURE=true` and `LETTA_SERVER_PASSWORD=yourpassword` in your `docker run` command:
-```sh
-# If LETTA_SERVER_PASSWORD isn't set, the server will autogenerate a password
-docker run \
- -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \
- -p 8283:8283 \
- --env-file .env \
- -e SECURE=true \
- -e LETTA_SERVER_PASSWORD=yourpassword \
- letta/letta:latest
-```
-
-With password protection enabled, you will have to provide your password in the bearer token header in your API requests:
-
-```typescript TypeScript maxLines=50
-// install letta-client with `npm install @letta-ai/letta-client`
-import { LettaClient } from '@letta-ai/letta-client'
-
-// create the client with the token set to your password
-const client = new LettaClient({
- baseUrl: "http://localhost:8283",
- token: "yourpassword"
-});
-```
-```python title="python" maxLines=50
-# install letta_client with `pip install letta-client`
-from letta_client import Letta
-
-# create the client with the token set to your password
-client = Letta(
- base_url="http://localhost:8283",
- token="yourpassword"
-)
-```
-```curl curl
-curl --request POST \
- --url http://localhost:8283/v1/agents/$AGENT_ID/messages \
- --header 'Content-Type: application/json' \
- --header 'Authorization: Bearer yourpassword' \
- --data '{
- "messages": [
- {
- "role": "user",
- "text": "hows it going????"
- }
- ]
-}'
-```
-
-
-
-## Tool sandboxing
-
-To enable tool sandboxing, set the `E2B_API_KEY` and `E2B_SANDBOX_TEMPLATE_ID` environment variables (via [E2B](https://e2b.dev/)) when you use `docker run`.
-When sandboxing is enabled, all custom tools (created by users from source code) will be executed in a sandboxed environment.
-
-This does not include MCP tools, which are executed outside of the Letta server (on the MCP server itself), or built-in tools (like `memory_insert`), whose code cannot be modified after server startup.
diff --git a/fern/pages/tutorials/draft/shared-memory-1-read-only.mdx b/fern/pages/tutorials/draft/shared-memory-1-read-only.mdx
deleted file mode 100644
index b10c480f..00000000
--- a/fern/pages/tutorials/draft/shared-memory-1-read-only.mdx
+++ /dev/null
@@ -1,828 +0,0 @@
----
-title: "Shared Memory Part 1: Read-Only Organizational Knowledge"
-subtitle: Build a hierarchical support team with shared company policies
-slug: cookbooks/shared-memory-read-only
----
-
-This tutorial demonstrates how to use shared read-only memory blocks to maintain consistent organizational knowledge across a hierarchical support team.
-
-## What You'll Learn
-
-- Creating read-only shared blocks for policies and procedures
-- Attaching shared blocks to multiple agents at creation time
-- Implementing hierarchical access (Tier 1 vs Tier 2 vs Tier 3)
-- Verifying agents cannot modify read-only content
-- Checking cross-agent consistency
-
-## Prerequisites
-
-```bash
-pip install letta-client
-```
-
-Set your Letta API key:
-```bash
-export LETTA_API_KEY="your-api-key-here"
-```
-
-## Part 1: Setup and Block Creation
-
-```python
-"""
-Tutorial 1: Read-Only Organizational Knowledge Base
-===================================================
-
-Build a customer support team with shared company policies.
-"""
-
-from letta import Letta
-
-# Initialize client
-client = Letta()
-
-print("=" * 70)
-print("TUTORIAL 1: Read-Only Organizational Knowledge Base")
-print("=" * 70)
-print()
-
-# Create Company Policies Block (Read-Only, All Tiers)
-print("STEP 1: Creating company policies block (read-only)...\n")
-
-company_policies = client.blocks.create(
- label="company_policies",
- description="Company-wide policies and procedures. Read-only to ensure consistency.",
- value="""
-=== COMPANY POLICIES ===
-
-Company: TechCorp Support Services
-Last Updated: 2024-10-08
-
-REFUND POLICY:
-- Standard products: 30-day money-back guarantee
-- Premium products: 60-day money-back guarantee
-- Digital products: 14-day refund window
-- Refund processing time: 5-7 business days
-
-RESPONSE TIME SLA:
-- Premium customers: 2 hours for initial response
-- Standard customers: 24 hours for initial response
-- After-hours: Next business day
-
-SUPPORT HOURS:
-- Monday-Friday: 9 AM - 9 PM EST
-- Saturday-Sunday: 10 AM - 6 PM EST
-- After-hours emergency line: Premium customers only
-
-CUSTOMER DATA POLICY:
-- Never share customer data across accounts
-- Always verify identity before discussing account details
-- PCI compliance required for payment information
-""",
- limit=5000
-)
-
-print(f"✓ Created block: {company_policies.id}")
-print(f" Label: {company_policies.label}")
-print(f" Read-only: True")
-print(f" Character count: {len(company_policies.value)}")
-print()
-
-# Create Escalation Procedures Block (Read-Only, Tier 2+ Only)
-print("STEP 2: Creating escalation procedures block (read-only, Tier 2+)...\n")
-
-escalation_procedures = client.blocks.create(
- label="escalation_procedures",
- description="Escalation procedures and protocols. Tier 2+ access only.",
- value="""
-=== ESCALATION PROCEDURES ===
-(Tier 2 and Tier 3 Access Only)
-
-ESCALATION CRITERIA:
-1. Customer requests manager/supervisor
-2. Issue involves billing dispute over $500
-3. Technical issue unresolved after 3 attempts
-4. Customer threatens legal action
-5. Security or privacy concern
-
-ESCALATION PATH:
-Tier 1 → Tier 2 (Senior Support)
-Tier 2 → Tier 3 (Supervisor)
-Tier 3 → Department Manager
-
-WHEN TO INVOLVE TIER 3:
-- VIP customer issues
-- Potential PR situations
-- Internal system failures
-- Cross-department coordination needed
-
-TIER 2 SPECIAL CAPABILITIES:
-- Can issue refunds up to $1,000 without approval
-- Can provide account credits up to $500
-- Access to customer order history (full)
-- Can escalate to engineering team
-
-TIER 3 SPECIAL CAPABILITIES:
-- Unlimited refund authority
-- Can waive policies in exceptional circumstances
-- Direct access to engineering and product teams
-- Incident report filing
-""",
- limit=4000
-)
-
-print(f"✓ Created block: {escalation_procedures.id}")
-print(f" Label: {escalation_procedures.label}")
-print(f" Read-only: True")
-print(f" Access: Tier 2 and Tier 3 only")
-print()
-```
-
-
-```
-======================================================================
-TUTORIAL 1: Read-Only Organizational Knowledge Base
-======================================================================
-
-STEP 1: Creating company policies block (read-only)...
-
-✓ Created block: block-a1b2c3d4-e5f6-7890-abcd-ef1234567890
- Label: company_policies
- Read-only: True
- Character count: 687
-
-STEP 2: Creating escalation procedures block (read-only, Tier 2+)...
-
-✓ Created block: block-b2c3d4e5-f6a7-8901-bcde-f12345678901
- Label: escalation_procedures
- Read-only: True
- Access: Tier 2 and Tier 3 only
-```
-
-
-## Part 2: Agent Creation
-
-Now let's create a hierarchical team structure where different tiers have access to different shared blocks.
-
-```python
-# Create Tier 1 Support Agents (Basic Support)
-print("STEP 3: Creating Tier 1 support agents...\n")
-
-tier1_agent_a = client.agents.create(
- name="Tier1_Support_Agent_A",
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am a Tier 1 customer support agent for TechCorp Support Services.
-
-My role:
-- Handle basic customer inquiries
-- Provide information about policies and procedures
-- Escalate complex issues to Tier 2
-- Always remain professional and helpful
-
-My limitations:
-- Cannot modify company policies
-- Cannot issue refunds (must escalate)
-- Cannot access escalation procedures (Tier 2+ only)
-"""
- },
- {
- "label": "human",
- "value": "Name: Customer\nRole: Customer seeking support"
- }
- ],
- block_ids=[company_policies.id], # Only basic policies
-)
-
-print(f"✓ Created Tier 1 Agent A: {tier1_agent_a.id}")
-print(f" Shared blocks: company_policies")
-print()
-
-tier1_agent_b = client.agents.create(
- name="Tier1_Support_Agent_B",
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am a Tier 1 customer support agent for TechCorp Support Services.
-
-My role:
-- Handle basic customer inquiries
-- Provide information about policies and procedures
-- Escalate complex issues to Tier 2
-- Always remain professional and helpful
-
-My limitations:
-- Cannot modify company policies
-- Cannot issue refunds (must escalate)
-- Cannot access escalation procedures (Tier 2+ only)
-"""
- },
- {
- "label": "human",
- "value": "Name: Customer\nRole: Customer seeking support"
- }
- ],
- block_ids=[company_policies.id],
-)
-
-print(f"✓ Created Tier 1 Agent B: {tier1_agent_b.id}")
-print(f" Shared blocks: company_policies")
-print()
-
-# Create Tier 2 Senior Support Agent
-print("STEP 4: Creating Tier 2 senior support agent...\n")
-
-tier2_agent = client.agents.create(
- name="Tier2_Senior_Support",
- model="openai/gpt-4o",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am a Tier 2 Senior Support agent for TechCorp Support Services.
-
-My role:
-- Handle complex technical and billing issues
-- Process refunds up to $1,000
-- Access full customer order history
-- Escalate critical issues to Tier 3 Supervisor
-
-My capabilities:
-- Issue account credits up to $500
-- Access escalation procedures
-- Coordinate with engineering team
-- Mentor Tier 1 agents
-
-My limitations:
-- Cannot modify company policies
-- Refunds over $1,000 require Tier 3 approval
-"""
- },
- {
- "label": "human",
- "value": "Name: Customer\nRole: Customer with escalated issue"
- }
- ],
- block_ids=[company_policies.id, escalation_procedures.id], # Both blocks!
-)
-
-print(f"✓ Created Tier 2 Agent: {tier2_agent.id}")
-print(f" Shared blocks: company_policies, escalation_procedures")
-print()
-
-# Create Tier 3 Supervisor
-print("STEP 5: Creating Tier 3 supervisor...\n")
-
-# First create a private team metrics block for the supervisor
-team_metrics = client.blocks.create(
- label="team_metrics",
- description="Private team performance metrics tracked by Tier 3 supervisor.",
- value="""
-=== TEAM METRICS ===
-Date: 2024-10-08
-
-Tier 1 Agent A: 0 tickets handled
-Tier 1 Agent B: 0 tickets handled
-Tier 2 Agent: 0 escalations handled
-
-Average response time: N/A
-Customer satisfaction: N/A
-""",
- limit=3000
-)
-
-tier3_supervisor = client.agents.create(
- name="Tier3_Supervisor",
- model="openai/gpt-4o",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am a Tier 3 Supervisor for TechCorp Support Services.
-
-My role:
-- Oversee support team performance
-- Handle critical escalations and VIP customers
-- Track team metrics and identify improvement areas
-- Make policy exception decisions
-- Coordinate cross-department issues
-
-My capabilities:
-- Unlimited refund authority
-- Can waive policies in exceptional circumstances
-- Access to all escalation procedures
-- Direct engineering and product team access
-- Track team performance metrics (private)
-
-My limitations:
-- Cannot modify company-wide policies (read-only)
-"""
- },
- {
- "label": "human",
- "value": "Name: Team Member or Customer\nRole: Supervisor oversight"
- }
- ],
- block_ids=[company_policies.id, escalation_procedures.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-# Attach the private team_metrics block
-client.agents.blocks.attach(
- agent_id=tier3_supervisor.id,
- block_id=team_metrics.id
-)
-
-print(f"✓ Created Tier 3 Supervisor: {tier3_supervisor.id}")
-print(f" Shared blocks: company_policies, escalation_procedures")
-print(f" Private blocks: team_metrics (read/write)")
-print()
-
-print("=" * 70)
-print("AGENT HIERARCHY CREATED")
-print("=" * 70)
-print(f"""
-Tier 1 Agent A → company_policies [R]
-Tier 1 Agent B → company_policies [R]
-Tier 2 Agent → company_policies [R], escalation_procedures [R]
-Tier 3 Super → company_policies [R], escalation_procedures [R], team_metrics [R/W]
-""")
-```
-
-### Block Access Matrix
-
-After creating all agents, here's the access matrix:
-
-| | company_policies | escalation_procedures | team_metrics |
-|---|---|---|---|
-| **Tier 1 Agent A** | ✓ Read | ✗ | ✗ |
-| **Tier 1 Agent B** | ✓ Read | ✗ | ✗ |
-| **Tier 2 Agent** | ✓ Read | ✓ Read | ✗ |
-| **Tier 3 Supervisor** | ✓ Read | ✓ Read | ✓ Read/Write |
-
-
-Notice how agents higher in the hierarchy have access to more shared blocks. This pattern is common in organizational structures where information access is tiered based on role and responsibility.
-
-
-## Part 3: Message Flow Scenarios
-
-### Scenario 1: Basic Policy Query
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 1: Basic Policy Query")
-print("=" * 70)
-print("\nUser → Tier 1 Agent A: 'What's your refund policy for premium products?'\n")
-
-response = client.agents.messages.create(
- agent_id=tier1_agent_a.id,
- messages=[{
- "role": "user",
- "content": "What's your refund policy for premium products?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Tier 1 Agent A: {msg.content}\n")
-
-print("Analysis:")
-print("- Agent accessed company_policies shared block")
-print("- Provided accurate 60-day refund policy for premium products")
-print("- Did not need to escalate (standard policy question)")
-```
-
-
-```
-======================================================================
-SCENARIO 1: Basic Policy Query
-======================================================================
-
-User → Tier 1 Agent A: 'What's your refund policy for premium products?'
-
-Tier 1 Agent A: Our refund policy for premium products offers a 60-day money-back
-guarantee. This means you can request a full refund within 60 days of purchase. The
-refund processing typically takes 5-7 business days. Is there anything specific about
-the refund process you'd like to know more about?
-
-Analysis:
-- Agent accessed company_policies shared block
-- Provided accurate 60-day refund policy for premium products
-- Did not need to escalate (standard policy question)
-```
-
-
-### Scenario 2: Cross-Agent Consistency
-
-This scenario demonstrates that both Tier 1 agents give consistent answers because they share the same `company_policies` block.
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 2: Cross-Agent Consistency")
-print("=" * 70)
-print("\nTesting that both Tier 1 agents give consistent answers...\n")
-
-print("User → Tier 1 Agent A: 'What's the response time SLA for standard customers?'\n")
-response_a = client.agents.messages.create(
- agent_id=tier1_agent_a.id,
- messages=[{
- "role": "user",
- "content": "What's the response time SLA for standard customers?"
- }]
-)
-
-for msg in response_a.messages:
- if msg.message_type == "assistant_message":
- print(f"Tier 1 Agent A: {msg.content}\n")
-
-print("User → Tier 1 Agent B: 'What's the response time SLA for standard customers?'\n")
-response_b = client.agents.messages.create(
- agent_id=tier1_agent_b.id,
- messages=[{
- "role": "user",
- "content": "What's the response time SLA for standard customers?"
- }]
-)
-
-for msg in response_b.messages:
- if msg.message_type == "assistant_message":
- print(f"Tier 1 Agent B: {msg.content}\n")
-
-print("Analysis:")
-print("- Both agents accessed the same company_policies block")
-print("- Both provided identical information (24 hours for standard customers)")
-print("- Demonstrates shared memory ensures consistency across agents")
-```
-
-
-```
-======================================================================
-SCENARIO 2: Cross-Agent Consistency
-======================================================================
-
-Testing that both Tier 1 agents give consistent answers...
-
-User → Tier 1 Agent A: 'What's the response time SLA for standard customers?'
-
-Tier 1 Agent A: For standard customers, our SLA is 24 hours for an initial response.
-We aim to get back to you within one business day of receiving your inquiry.
-
-User → Tier 1 Agent B: 'What's the response time SLA for standard customers?'
-
-Tier 1 Agent B: Our response time SLA for standard customers is 24 hours for the
-initial response. You can expect to hear back from us within one business day.
-
-Analysis:
-- Both agents accessed the same company_policies block
-- Both provided identical information (24 hours for standard customers)
-- Demonstrates shared memory ensures consistency across agents
-```
-
-
-
-This consistency is crucial in customer support scenarios. Without shared memory, agents might give conflicting information, leading to customer confusion and reduced trust.
-
-
-### Scenario 3: Tier 2 Accesses Escalation Procedures
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 3: Tier 2 Accesses Escalation Procedures")
-print("=" * 70)
-print("\nUser → Tier 2 Agent: 'A customer is requesting a $750 refund. What can I do?'\n")
-
-response = client.agents.messages.create(
- agent_id=tier2_agent.id,
- messages=[{
- "role": "user",
- "content": "A customer is requesting a $750 refund. What can I do?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Tier 2 Agent: {msg.content}\n")
-
-print("Analysis:")
-print("- Tier 2 agent accessed escalation_procedures block")
-print("- Correctly identified $750 is within Tier 2 authority ($1,000 limit)")
-print("- Tier 1 agents would NOT have this information")
-```
-
-
-```
-======================================================================
-SCENARIO 3: Tier 2 Accesses Escalation Procedures
-======================================================================
-
-User → Tier 2 Agent: 'A customer is requesting a $750 refund. What can I do?'
-
-Tier 2 Agent: I can help with this. As a Tier 2 agent, I have the authority to issue
-refunds up to $1,000 without requiring approval. Since the customer is requesting $750,
-I can process this directly.
-
-To proceed, I'll need to:
-1. Verify the customer's identity and order details
-2. Confirm the refund qualifies under our policy (check purchase date against our refund windows)
-3. Process the refund, which will take 5-7 business days
-
-Would you like me to proceed with this refund?
-
-Analysis:
-- Tier 2 agent accessed escalation_procedures block
-- Correctly identified $750 is within Tier 2 authority ($1,000 limit)
-- Tier 1 agents would NOT have this information
-```
-
-
-### Scenario 4: Attempting to Modify Read-Only Block
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 4: Attempting to Modify Read-Only Block")
-print("=" * 70)
-print("\nUser → Tier 1 Agent A: 'Update the refund policy to 90 days for premium products'\n")
-
-response = client.agents.messages.create(
- agent_id=tier1_agent_a.id,
- messages=[{
- "role": "user",
- "content": "Update the refund policy to 90 days for premium products"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Tier 1 Agent A: {msg.content}\n")
-
-print("Analysis:")
-print("- Agent attempted to use core_memory_replace on company_policies block")
-print("- Operation failed because block is read-only")
-print("- Agent explained it cannot modify company policies")
-```
-
-
-```
-======================================================================
-SCENARIO 4: Attempting to Modify Read-Only Block
-======================================================================
-
-User → Tier 1 Agent A: 'Update the refund policy to 90 days for premium products'
-
-Tier 1 Agent A: I'm unable to modify our company policies. Our refund policies are set
-at the corporate level and I don't have the authority to change them. The current policy
-for premium products is a 60-day money-back guarantee.
-
-If you believe our policy should be reconsidered, I can escalate your feedback to
-management. Would you like me to do that?
-
-Analysis:
-- Agent attempted to use core_memory_replace on company_policies block
-- Operation failed because block is read-only
-- Agent explained it cannot modify company policies
-```
-
-
-
-Read-only blocks are essential for protecting critical information. Without this protection, agents could accidentally (or through user manipulation) modify important policies, leading to inconsistencies and potential liability issues.
-
-
-### Scenario 5: Supervisor Updates Private Metrics
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 5: Supervisor Updates Private Team Metrics")
-print("=" * 70)
-print("\nUser → Tier 3 Supervisor: 'Log that Tier 1 Agent A handled 5 tickets today with 95% satisfaction'\n")
-
-response = client.agents.messages.create(
- agent_id=tier3_supervisor.id,
- messages=[{
- "role": "user",
- "content": "Log that Tier 1 Agent A handled 5 tickets today with 95% customer satisfaction"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Tier 3 Supervisor: {msg.content}\n")
-
-# Now check that the team_metrics block was updated
-updated_metrics = client.blocks.retrieve(block_id=team_metrics.id)
-print("Updated team_metrics block:")
-print("-" * 70)
-print(updated_metrics.value)
-print("-" * 70)
-
-print("\nAnalysis:")
-print("- Supervisor updated team_metrics (private read/write block)")
-print("- Other agents (Tier 1, Tier 2) do NOT have access to this block")
-print("- Demonstrates mixed read-only shared + private read/write blocks")
-```
-
-
-```
-======================================================================
-SCENARIO 5: Supervisor Updates Private Team Metrics
-======================================================================
-
-User → Tier 3 Supervisor: 'Log that Tier 1 Agent A handled 5 tickets today with 95% satisfaction'
-
-Tier 3 Supervisor: I've updated the team metrics to reflect that Tier 1 Agent A handled
-5 tickets today with an excellent 95% customer satisfaction rating. This is great
-performance and shows strong customer service skills.
-
-Updated team_metrics block:
-----------------------------------------------------------------------
-=== TEAM METRICS ===
-Date: 2024-10-08
-
-Tier 1 Agent A: 5 tickets handled (95% customer satisfaction)
-Tier 1 Agent B: 0 tickets handled
-Tier 2 Agent: 0 escalations handled
-
-Average response time: N/A
-Customer satisfaction: 95% (Agent A)
-----------------------------------------------------------------------
-
-Analysis:
-- Supervisor updated team_metrics (private read/write block)
-- Other agents (Tier 1, Tier 2) do NOT have access to this block
-- Demonstrates mixed read-only shared + private read/write blocks
-```
-
-
-### Scenario 6: Information Hierarchy
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 6: Testing Information Hierarchy")
-print("=" * 70)
-print("\nUser → Tier 1 Agent A: 'What are the escalation criteria?'\n")
-
-response = client.agents.messages.create(
- agent_id=tier1_agent_a.id,
- messages=[{
- "role": "user",
- "content": "What are the escalation criteria for moving a ticket to Tier 2?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Tier 1 Agent A: {msg.content}\n")
-
-print("Analysis:")
-print("- Tier 1 agent does NOT have access to escalation_procedures block")
-print("- Agent can only provide general guidance, not detailed procedures")
-print("- This protects sensitive operational information")
-```
-
-
-```
-======================================================================
-SCENARIO 6: Testing Information Hierarchy
-======================================================================
-
-User → Tier 1 Agent A: 'What are the escalation criteria?'
-
-Tier 1 Agent A: As a Tier 1 agent, I know that I should escalate issues to Tier 2 when
-they're complex or technical in nature, but I don't have access to the detailed escalation
-procedures.
-
-Generally, I escalate when:
-- The issue is beyond my authority to resolve
-- A customer specifically requests a manager or supervisor
-- The situation requires specialized technical knowledge
-
-Would you like me to escalate your current issue to a senior support agent?
-
-Analysis:
-- Tier 1 agent does NOT have access to escalation_procedures block
-- Agent can only provide general guidance, not detailed procedures
-- This protects sensitive operational information
-```
-
-
-## Part 4: Block Inspection and Verification
-
-```python
-# Check which agents use company_policies
-print("\n1. Checking company_policies block usage...")
-policies_info = client.blocks.retrieve(block_id=company_policies.id)
-print(f"\nBlock: {policies_info.label}")
-print(f"Used by {len(policies_info.agent_ids)} agents:")
-for agent_id in policies_info.agent_ids:
- agent_info = client.agents.retrieve(agent_id=agent_id)
- print(f" - {agent_info.name} ({agent_id})")
-
-# Check which agents use escalation_procedures
-print("\n2. Checking escalation_procedures block usage...")
-escalation_info = client.blocks.retrieve(block_id=escalation_procedures.id)
-print(f"\nBlock: {escalation_info.label}")
-print(f"Used by {len(escalation_info.agent_ids)} agents:")
-for agent_id in escalation_info.agent_ids:
- agent_info = client.agents.retrieve(agent_id=agent_id)
- print(f" - {agent_info.name} ({agent_id})")
-
-# Check team_metrics (should be supervisor only)
-print("\n3. Checking team_metrics block usage...")
-metrics_info = client.blocks.retrieve(block_id=team_metrics.id)
-print(f"\nBlock: {metrics_info.label}")
-print(f"Used by {len(metrics_info.agent_ids)} agent(s):")
-for agent_id in metrics_info.agent_ids:
- agent_info = client.agents.retrieve(agent_id=agent_id)
- print(f" - {agent_info.name} ({agent_id})")
-
-# Attempt API-Level Modification of Read-Only Block
-print("\n" + "=" * 70)
-print("INSPECTION: Testing Read-Only Protection")
-print("=" * 70)
-
-print("\nAttempting to modify read-only company_policies block via API...")
-
-try:
- client.blocks.update(
- block_id=company_policies.id,
- value="MODIFIED POLICY: Everything is free!"
- )
- print("❌ ERROR: Block modification succeeded (should have failed!)")
-except Exception as e:
- print(f"✓ Block modification prevented: {str(e)}")
- print("This is expected behavior for read-only blocks.")
-```
-
-
-```
-======================================================================
-INSPECTION: Block Usage Analysis
-======================================================================
-
-1. Checking company_policies block usage...
-
-Block: company_policies
-Used by 4 agents:
- - Tier1_Support_Agent_A (agent-12345678-abcd-ef01-2345-67890abcdef1)
- - Tier1_Support_Agent_B (agent-23456789-bcde-f012-3456-7890abcdef12)
- - Tier2_Senior_Support (agent-34567890-cdef-0123-4567-890abcdef123)
- - Tier3_Supervisor (agent-45678901-def0-1234-5678-90abcdef1234)
-
-2. Checking escalation_procedures block usage...
-
-Block: escalation_procedures
-Used by 2 agents:
- - Tier2_Senior_Support (agent-34567890-cdef-0123-4567-890abcdef123)
- - Tier3_Supervisor (agent-45678901-def0-1234-5678-90abcdef1234)
-
-3. Checking team_metrics block usage...
-
-Block: team_metrics
-Used by 1 agent(s):
- - Tier3_Supervisor (agent-45678901-def0-1234-5678-90abcdef1234)
-
-======================================================================
-INSPECTION: Testing Read-Only Protection
-======================================================================
-
-Attempting to modify read-only company_policies block via API...
-✓ Block modification prevented: Block is read-only and cannot be modified
-This is expected behavior for read-only blocks.
-```
-
-
-## Key Takeaways
-
-
-
-Protect critical information from accidental modification by agents or users
-
-
-
-Different agent tiers can access different combinations of shared blocks
-
-
-
-All agents with access to the same block see identical information
-
-
-
-Combine shared read-only blocks with private read/write blocks for flexibility
-
-
-
-### Patterns Demonstrated
-
-1. **Read-only shared blocks** ensure consistent information across agents
-2. **Hierarchical access control** (Tier 1 < Tier 2 < Tier 3)
-3. **Mixed block types** (shared read-only + private read/write)
-4. **Information cascade** (company-wide → department → individual)
-
-## Next Steps
-
-
-Learn how to use read/write shared blocks for worker coordination and task management
-
diff --git a/fern/pages/tutorials/draft/shared-memory-2-task-coordination.mdx b/fern/pages/tutorials/draft/shared-memory-2-task-coordination.mdx
deleted file mode 100644
index f1743fd5..00000000
--- a/fern/pages/tutorials/draft/shared-memory-2-task-coordination.mdx
+++ /dev/null
@@ -1,1443 +0,0 @@
----
-title: "Shared Memory Part 2: Task Coordination with Shared Queues"
-subtitle: Build a data analysis team with shared task coordination
-slug: cookbooks/shared-memory-task-coordination
----
-
-This tutorial demonstrates how to use shared read/write memory blocks for task coordination in a multi-agent worker system.
-
-## What You'll Learn
-
-- Creating read/write shared blocks that multiple agents can update
-- Coordinating work across multiple worker agents
-- Implementing supervisor-worker patterns
-- Handling concurrent updates to shared state
-- Combining shared task queues with private work logs
-
-## Prerequisites
-
-```bash
-pip install letta-client
-```
-
-Set your Letta API key:
-```bash
-export LETTA_API_KEY="your-api-key-here"
-```
-
-## Part 1: Setup and Block Creation
-
-```python
-"""
-Tutorial 2: Task Coordination with Shared Queues
-================================================
-
-Build a data analysis team with shared task queue and completion tracking.
-"""
-
-from letta import Letta
-from datetime import datetime
-
-# Initialize client
-client = Letta()
-
-print("=" * 70)
-print("TUTORIAL 2: Task Coordination with Shared Queues")
-print("=" * 70)
-print()
-
-# Create Shared Task Queue (Read/Write, All Team Members)
-print("STEP 1: Creating shared task queue block (read/write)...\n")
-
-task_queue = client.blocks.create(
- label="task_queue",
- description="Shared task queue for team coordination. All agents can read and update.",
- value="""
-=== ACTIVE TASK QUEUE ===
-Last Updated: 2024-10-08 09:00 AM
-
-PENDING TASKS:
----------------
-[TASK-001] Analyze Q4 Sales Data
- Priority: HIGH
- Status: Unassigned
- Est. Time: 2 hours
- Description: Analyze Q4 sales trends, identify top products, regional performance
-
-[TASK-002] Generate Customer Churn Report
- Priority: MEDIUM
- Status: Unassigned
- Est. Time: 3 hours
- Description: Identify at-risk customers, calculate churn rate, recommend retention strategies
-
-[TASK-003] Competitor Research Summary
- Priority: MEDIUM
- Status: Unassigned
- Est. Time: 1.5 hours
- Description: Research top 3 competitors, pricing analysis, feature comparison
-
-[TASK-004] Website Traffic Analysis
- Priority: LOW
- Status: Unassigned
- Est. Time: 1 hour
- Description: Analyze website traffic patterns, conversion rates, bounce rates
-
-IN PROGRESS:
----------------
-(none)
-
-BLOCKED:
----------------
-(none)
-""",
- limit=10000
-)
-
-print(f"✓ Created block: {task_queue.id}")
-print(f" Label: {task_queue.label}")
-print(f" Read/Write: True")
-print(f" Accessible by: Supervisor + All Workers")
-print(f" Initial tasks: 4 pending")
-print()
-
-# Create Shared Completed Work Log (Read/Write, All Team Members)
-print("STEP 2: Creating shared completed work log (read/write)...\n")
-
-completed_work = client.blocks.create(
- label="completed_work",
- description="Shared log of completed tasks with summaries and findings.",
- value="""
-=== COMPLETED WORK LOG ===
-Last Updated: 2024-10-08 09:00 AM
-
-COMPLETED TASKS:
----------------
-(no completed tasks yet)
-
-TOTAL COMPLETED: 0
-AVERAGE COMPLETION TIME: N/A
-""",
- limit=12000
-)
-
-print(f"✓ Created block: {completed_work.id}")
-print(f" Label: {completed_work.label}")
-print(f" Read/Write: True")
-print(f" Purpose: Track completed work and share findings across team")
-print()
-
-# Create Team Metrics Block (Supervisor Only)
-print("STEP 3: Creating supervisor-only team metrics block...\n")
-
-team_metrics = client.blocks.create(
- label="team_metrics",
- description="Team performance metrics. Supervisor access only.",
- value="""
-=== TEAM PERFORMANCE METRICS ===
-Date: 2024-10-08
-
-WORKER STATISTICS:
-------------------
-Data Analyst Worker 1: 0 tasks completed
-Data Analyst Worker 2: 0 tasks completed
-Research Worker 3: 0 tasks completed
-
-TEAM TOTALS:
-------------------
-Total tasks assigned: 4
-Total tasks completed: 0
-Total tasks in progress: 0
-Total tasks blocked: 0
-
-PERFORMANCE:
-------------------
-Average completion time: N/A
-Team velocity: N/A
-Current utilization: 0%
-
-NOTES:
-------------------
-Team just started, baseline metrics to be established.
-""",
- limit=8000
-)
-
-print(f"✓ Created block: {team_metrics.id}")
-print(f" Label: {team_metrics.label}")
-print(f" Read/Write: True")
-print(f" Access: Supervisor only (private)")
-print()
-```
-
-
-```
-======================================================================
-TUTORIAL 2: Task Coordination with Shared Queues
-======================================================================
-
-STEP 1: Creating shared task queue block (read/write)...
-
-✓ Created block: block-f1e2d3c4-b5a6-9870-cdef-ab1234567890
- Label: task_queue
- Read/Write: True
- Accessible by: Supervisor + All Workers
- Initial tasks: 4 pending
-
-STEP 2: Creating shared completed work log (read/write)...
-
-✓ Created block: block-g2f3e4d5-c6b7-0981-defg-bc2345678901
- Label: completed_work
- Read/Write: True
- Purpose: Track completed work and share findings across team
-
-STEP 3: Creating supervisor-only team metrics block...
-
-✓ Created block: block-h3g4f5e6-d7c8-1092-efgh-cd3456789012
- Label: team_metrics
- Read/Write: True
- Access: Supervisor only (private)
-```
-
-
-## Part 2: Agent Creation
-
-Now let's create a project supervisor and three specialized workers.
-
-```python
-# Create Project Supervisor
-print("STEP 4: Creating project supervisor...\n")
-
-supervisor = client.agents.create(
- name="Project_Supervisor",
- model="openai/gpt-4o",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am a Project Supervisor managing a data analytics team.
-
-My role:
-- Assign tasks to team members
-- Monitor task progress and team performance
-- Track team metrics and productivity
-- Unblock workers and resolve issues
-- Coordinate work distribution
-
-My capabilities:
-- Update task_queue (assign, modify priorities, reassign)
-- Read completed_work (review team output)
-- Update team_metrics (track performance - PRIVATE)
-- Reassign blocked tasks
-- Provide guidance to workers
-
-My style:
-- Clear and direct communication
-- Encourage team collaboration
-- Data-driven decision making
-- Supportive leadership
-"""
- },
- {
- "label": "human",
- "value": "Name: Team Manager\nRole: Managing the analytics team"
- }
- ],
- block_ids=[task_queue.id, completed_work.id, team_metrics.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created Supervisor: {supervisor.id}")
-print(f" Name: {supervisor.name}")
-print(f" Shared blocks: task_queue, completed_work")
-print(f" Private blocks: team_metrics")
-print()
-
-# Create Worker 1 - Data Analyst
-print("STEP 5: Creating Data Analyst Worker 1...\n")
-
-worker1_log = client.blocks.create(
- label="worker1_work_log",
- description="Private work log for Data Analyst Worker 1.",
- value="""
-=== WORKER 1 PRIVATE WORK LOG ===
-Date: 2024-10-08
-
-CURRENT TASK: None
-TASKS COMPLETED TODAY: 0
-
-PERSONAL NOTES:
----------------
-Ready to start work. Specialization: Sales and financial data analysis.
-""",
- limit=5000
-)
-
-worker1 = client.agents.create(
- name="Data_Analyst_Worker_1",
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am Data Analyst Worker 1, specializing in sales and financial analysis.
-
-My role:
-- Claim tasks from the shared task queue
-- Complete data analysis tasks
-- Update task status in shared queue
-- Log completed work with findings
-- Maintain my private work log
-
-My capabilities:
-- Read task_queue (see available tasks)
-- Update task_queue (claim tasks, update status)
-- Update completed_work (share findings)
-- Update my private work_log (personal notes)
-
-My specialties:
-- Sales data analysis
-- Financial metrics and reporting
-- Revenue trend analysis
-- Performance dashboards
-
-My style:
-- Detail-oriented and thorough
-- Data-driven insights
-- Clear documentation
-"""
- },
- {
- "label": "human",
- "value": "Name: Project Supervisor\nRole: Managing my work"
- }
- ],
- block_ids=[task_queue.id, completed_work.id, worker1_log.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created Worker 1: {worker1.id}")
-print(f" Specialty: Sales and financial analysis")
-print()
-
-# Create Worker 2 - Data Analyst
-print("STEP 6: Creating Data Analyst Worker 2...\n")
-
-worker2_log = client.blocks.create(
- label="worker2_work_log",
- description="Private work log for Data Analyst Worker 2.",
- value="""
-=== WORKER 2 PRIVATE WORK LOG ===
-Date: 2024-10-08
-
-CURRENT TASK: None
-TASKS COMPLETED TODAY: 0
-
-PERSONAL NOTES:
----------------
-Ready to start work. Specialization: Customer behavior and web analytics.
-""",
- limit=5000
-)
-
-worker2 = client.agents.create(
- name="Data_Analyst_Worker_2",
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am Data Analyst Worker 2, specializing in customer behavior and web analytics.
-
-My role:
-- Claim tasks from the shared task queue
-- Complete data analysis tasks
-- Update task status in shared queue
-- Log completed work with findings
-- Maintain my private work log
-
-My capabilities:
-- Read task_queue (see available tasks)
-- Update task_queue (claim tasks, update status)
-- Update completed_work (share findings)
-- Update my private work_log (personal notes)
-
-My specialties:
-- Customer churn analysis
-- Web traffic and conversion analytics
-- User behavior patterns
-- Retention strategies
-
-My style:
-- Customer-focused insights
-- Actionable recommendations
-- Visual data presentation
-"""
- },
- {
- "label": "human",
- "value": "Name: Project Supervisor\nRole: Managing my work"
- }
- ],
- block_ids=[task_queue.id, completed_work.id, worker2_log.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created Worker 2: {worker2.id}")
-print(f" Specialty: Customer behavior and web analytics")
-print()
-
-# Create Worker 3 - Research Worker
-print("STEP 7: Creating Research Worker 3...\n")
-
-worker3_log = client.blocks.create(
- label="worker3_work_log",
- description="Private work log for Research Worker 3.",
- value="""
-=== WORKER 3 PRIVATE WORK LOG ===
-Date: 2024-10-08
-
-CURRENT TASK: None
-TASKS COMPLETED TODAY: 0
-
-PERSONAL NOTES:
----------------
-Ready to start work. Specialization: Market research and competitive analysis.
-""",
- limit=5000
-)
-
-worker3 = client.agents.create(
- name="Research_Worker_3",
- model="anthropic/claude-3-5-sonnet-20241022",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am Research Worker 3, specializing in market research and competitive analysis.
-
-My role:
-- Claim tasks from the shared task queue
-- Complete research tasks
-- Update task status in shared queue
-- Log completed work with findings
-- Maintain my private work log
-
-My capabilities:
-- Read task_queue (see available tasks)
-- Update task_queue (claim tasks, update status)
-- Update completed_work (share findings)
-- Update my private work_log (personal notes)
-- Conduct web research (when needed)
-
-My specialties:
-- Competitive analysis
-- Market research
-- Industry trends
-- Strategic insights
-
-My style:
-- Comprehensive research
-- Strategic thinking
-- Well-sourced insights
-"""
- },
- {
- "label": "human",
- "value": "Name: Project Supervisor\nRole: Managing my work"
- }
- ],
- block_ids=[task_queue.id, completed_work.id, worker3_log.id],
- tools=["core_memory_append", "core_memory_replace", "web_search"],
-)
-
-print(f"✓ Created Worker 3: {worker3.id}")
-print(f" Specialty: Market research and competitive analysis")
-print()
-
-print("=" * 70)
-print("TEAM STRUCTURE CREATED")
-print("=" * 70)
-print("""
-BLOCK ACCESS MATRIX:
- task_queue completed_work team_metrics work_log
-Supervisor R/W R/W R/W -
-Data Analyst Worker 1 R/W R/W - R/W (own)
-Data Analyst Worker 2 R/W R/W - R/W (own)
-Research Worker 3 R/W R/W - R/W (own)
-
-SHARED BLOCKS (All team members):
-- task_queue: Coordinate work assignments and status
-- completed_work: Share findings and deliverables
-
-PRIVATE BLOCKS:
-- team_metrics: Supervisor only (team performance tracking)
-- work_log: Each worker has their own (personal notes)
-""")
-```
-
-
-Unlike Tutorial 1 where blocks were read-only, here the `task_queue` and `completed_work` blocks are **read/write** for all team members. This enables dynamic coordination where workers can claim tasks and share results.
-
-
-## Part 3: Message Flow Scenarios
-
-### Scenario 1: Supervisor Adds New Task
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 1: Supervisor Adds Urgent Task")
-print("=" * 70)
-print("\nUser → Supervisor: 'Add a new HIGH priority task: Create executive dashboard for tomorrow's board meeting'\n")
-
-response = client.agents.messages.create(
- agent_id=supervisor.id,
- messages=[{
- "role": "user",
- "content": "Add a new HIGH priority task to the queue: Create executive dashboard for tomorrow's board meeting. Estimated 2 hours. This is urgent."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Supervisor: {msg.content}\n")
-
-# Check updated task queue
-updated_queue = client.blocks.retrieve(block_id=task_queue.id)
-print("Updated task_queue block (showing PENDING TASKS section):")
-print("-" * 70)
-pending_section = updated_queue.value.split("IN PROGRESS:")[0]
-print(pending_section)
-print("-" * 70)
-
-print("\nAnalysis:")
-print("- Supervisor updated shared task_queue block")
-print("- Added TASK-005 with HIGH priority")
-print("- All workers can now see this new task")
-print("- Change is immediately visible to entire team")
-```
-
-
-```
-======================================================================
-SCENARIO 1: Supervisor Adds Urgent Task
-======================================================================
-
-User → Supervisor: 'Add a new HIGH priority task: Create executive dashboard for tomorrow's board meeting'
-
-Supervisor: I've added a new HIGH priority task to the queue:
-
-[TASK-005] Create Executive Dashboard
- Priority: HIGH
- Status: Unassigned
- Est. Time: 2 hours
- Description: Create executive dashboard for tomorrow's board meeting
-
-This task is now visible to all team members and ready to be claimed. Given the urgency, I recommend assigning this to our most available worker.
-
-Updated task_queue block (showing PENDING TASKS section):
-----------------------------------------------------------------------
-=== ACTIVE TASK QUEUE ===
-Last Updated: 2024-10-08 09:15 AM
-
-PENDING TASKS:
----------------
-[TASK-001] Analyze Q4 Sales Data
- Priority: HIGH
- Status: Unassigned
- Est. Time: 2 hours
- Description: Analyze Q4 sales trends, identify top products, regional performance
-
-[TASK-002] Generate Customer Churn Report
- Priority: MEDIUM
- Status: Unassigned
- Est. Time: 3 hours
- Description: Identify at-risk customers, calculate churn rate, recommend retention strategies
-
-[TASK-003] Competitor Research Summary
- Priority: MEDIUM
- Status: Unassigned
- Est. Time: 1.5 hours
- Description: Research top 3 competitors, pricing analysis, feature comparison
-
-[TASK-004] Website Traffic Analysis
- Priority: LOW
- Status: Unassigned
- Est. Time: 1 hour
- Description: Analyze website traffic patterns, conversion rates, bounce rates
-
-[TASK-005] Create Executive Dashboard
- Priority: HIGH
- Status: Unassigned
- Est. Time: 2 hours
- Description: Create executive dashboard for tomorrow's board meeting
-
-----------------------------------------------------------------------
-
-Analysis:
-- Supervisor updated shared task_queue block
-- Added TASK-005 with HIGH priority
-- All workers can now see this new task
-- Change is immediately visible to entire team
-```
-
-
-### Scenario 2: Worker 1 Claims Sales Analysis Task
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 2: Worker 1 Claims Task")
-print("=" * 70)
-print("\nUser → Worker 1: 'Check the task queue and claim the Q4 Sales Data analysis task (TASK-001)'\n")
-
-response = client.agents.messages.create(
- agent_id=worker1.id,
- messages=[{
- "role": "user",
- "content": "Check the task queue and claim the Q4 Sales Data analysis task (TASK-001). Update the status to show you're working on it."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Worker 1: {msg.content}\n")
-
-# Check updated task queue
-updated_queue = client.blocks.retrieve(block_id=task_queue.id)
-print("Updated task_queue block (showing IN PROGRESS section):")
-print("-" * 70)
-in_progress_section = updated_queue.value.split("IN PROGRESS:")[1].split("BLOCKED:")[0]
-print("IN PROGRESS:")
-print(in_progress_section)
-print("-" * 70)
-
-# Check Worker 1's private log
-worker1_log_updated = client.blocks.retrieve(block_id=worker1_log.id)
-print("\nWorker 1's private work_log:")
-print("-" * 70)
-print(worker1_log_updated.value[:400] + "...")
-print("-" * 70)
-
-print("\nAnalysis:")
-print("- Worker 1 updated shared task_queue (moved TASK-001 to IN PROGRESS)")
-print("- Worker 1 also updated their private work_log")
-print("- Supervisor and other workers can see TASK-001 is now claimed")
-print("- Worker's private notes are NOT visible to others")
-```
-
-
-```
-======================================================================
-SCENARIO 2: Worker 1 Claims Task
-======================================================================
-
-User → Worker 1: 'Check the task queue and claim the Q4 Sales Data analysis task (TASK-001)'
-
-Worker 1: I've claimed TASK-001 (Analyze Q4 Sales Data) from the queue. This task aligns perfectly with my specialization in sales and financial analysis. I've updated the task status to "In Progress - Worker 1" and I'm starting work on it now.
-
-I'll analyze Q4 sales trends, identify top-performing products, and evaluate regional performance. Estimated completion time is 2 hours.
-
-Updated task_queue block (showing IN PROGRESS section):
-----------------------------------------------------------------------
-IN PROGRESS:
----------------
-[TASK-001] Analyze Q4 Sales Data
- Priority: HIGH
- Status: In Progress - Data Analyst Worker 1
- Est. Time: 2 hours
- Started: 2024-10-08 09:20 AM
- Description: Analyze Q4 sales trends, identify top products, regional performance
-
-----------------------------------------------------------------------
-
-Worker 1's private work_log:
-----------------------------------------------------------------------
-=== WORKER 1 PRIVATE WORK LOG ===
-Date: 2024-10-08
-
-CURRENT TASK: TASK-001 - Analyze Q4 Sales Data
-TASKS COMPLETED TODAY: 0
-
-PERSONAL NOTES:
----------------
-Started TASK-001 at 09:20 AM. This is a high-priority sales analysis task.
-Plan: Pull Q4 data, segment by product and region, identify trends...
-----------------------------------------------------------------------
-
-Analysis:
-- Worker 1 updated shared task_queue (moved TASK-001 to IN PROGRESS)
-- Worker 1 also updated their private work_log
-- Supervisor and other workers can see TASK-001 is now claimed
-- Worker's private notes are NOT visible to others
-```
-
-
-
-Notice how Worker 1 updates **both** the shared `task_queue` (visible to everyone) and their private `work_log` (visible only to them). This pattern allows for team coordination while maintaining personal notes.
-
-
-### Scenario 3: Worker 2 Claims Different Task (Concurrent Access)
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 3: Worker 2 Claims Different Task (Concurrent Access)")
-print("=" * 70)
-print("\nUser → Worker 2: 'Claim the Customer Churn Report task (TASK-002)'\n")
-
-response = client.agents.messages.create(
- agent_id=worker2.id,
- messages=[{
- "role": "user",
- "content": "Claim the Customer Churn Report task (TASK-002) from the queue. Update your status and start working on it."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Worker 2: {msg.content}\n")
-
-# Check updated task queue
-updated_queue = client.blocks.retrieve(block_id=task_queue.id)
-print("Updated task_queue block (showing IN PROGRESS section):")
-print("-" * 70)
-in_progress_section = updated_queue.value.split("IN PROGRESS:")[1].split("BLOCKED:")[0]
-print("IN PROGRESS:")
-print(in_progress_section)
-print("-" * 70)
-
-print("\nAnalysis:")
-print("- Worker 2 successfully claimed TASK-002")
-print("- Both TASK-001 (Worker 1) and TASK-002 (Worker 2) now in progress")
-print("- Shared block handled concurrent updates")
-print("- No task conflicts (different workers, different tasks)")
-```
-
-
-```
-======================================================================
-SCENARIO 3: Worker 2 Claims Different Task (Concurrent Access)
-======================================================================
-
-User → Worker 2: 'Claim the Customer Churn Report task (TASK-002)'
-
-Worker 2: Perfect! I've claimed TASK-002 (Generate Customer Churn Report). This aligns with my specialization in customer behavior analytics. I've updated the task queue to show I'm working on it.
-
-I'll be identifying at-risk customers, calculating churn rates, and developing retention strategy recommendations. Estimated completion: 3 hours.
-
-Updated task_queue block (showing IN PROGRESS section):
-----------------------------------------------------------------------
-IN PROGRESS:
----------------
-[TASK-001] Analyze Q4 Sales Data
- Priority: HIGH
- Status: In Progress - Data Analyst Worker 1
- Est. Time: 2 hours
- Started: 2024-10-08 09:20 AM
- Description: Analyze Q4 sales trends, identify top products, regional performance
-
-[TASK-002] Generate Customer Churn Report
- Priority: MEDIUM
- Status: In Progress - Data Analyst Worker 2
- Est. Time: 3 hours
- Started: 2024-10-08 09:25 AM
- Description: Identify at-risk customers, calculate churn rate, recommend retention strategies
-
-----------------------------------------------------------------------
-
-Analysis:
-- Worker 2 successfully claimed TASK-002
-- Both TASK-001 (Worker 1) and TASK-002 (Worker 2) now in progress
-- Shared block handled concurrent updates
-- No task conflicts (different workers, different tasks)
-```
-
-
-
-In production systems with high concurrency, you may want to implement additional locking mechanisms or use optimistic concurrency control to prevent race conditions when multiple agents try to claim the same task simultaneously.
-
-
-### Scenario 4: Supervisor Checks Progress
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 4: Supervisor Checks Team Progress")
-print("=" * 70)
-print("\nUser → Supervisor: 'What tasks are currently in progress? Who's working on what?'\n")
-
-response = client.agents.messages.create(
- agent_id=supervisor.id,
- messages=[{
- "role": "user",
- "content": "Give me a status update. What tasks are currently in progress and who is working on them?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Supervisor: {msg.content}\n")
-
-print("Analysis:")
-print("- Supervisor read the shared task_queue block")
-print("- Saw real-time updates from both workers")
-print("- Supervisor did NOT see workers' private work logs")
-print("- Demonstrates shared block visibility for coordination")
-```
-
-
-```
-======================================================================
-SCENARIO 4: Supervisor Checks Team Progress
-======================================================================
-
-User → Supervisor: 'What tasks are currently in progress? Who's working on what?'
-
-Supervisor: Here's the current team status:
-
-**In Progress (2 tasks):**
-1. TASK-001: Analyze Q4 Sales Data
- - Assigned to: Data Analyst Worker 1
- - Priority: HIGH
- - Started: 09:20 AM
- - Est. completion: ~11:20 AM (2 hours)
-
-2. TASK-002: Generate Customer Churn Report
- - Assigned to: Data Analyst Worker 2
- - Priority: MEDIUM
- - Started: 09:25 AM
- - Est. completion: ~12:25 PM (3 hours)
-
-**Available Workers:**
-- Research Worker 3 (available for assignment)
-
-**Pending High-Priority Tasks:**
-- TASK-005: Create Executive Dashboard (URGENT - for tomorrow's board meeting)
-
-I recommend assigning TASK-005 to Research Worker 3 or waiting for Worker 1 to complete their current task.
-
-Analysis:
-- Supervisor read the shared task_queue block
-- Saw real-time updates from both workers
-- Supervisor did NOT see workers' private work logs
-- Demonstrates shared block visibility for coordination
-```
-
-
-### Scenario 5: Worker 1 Completes Task and Shares Findings
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 5: Worker 1 Completes Task and Shares Findings")
-print("=" * 70)
-print("\nUser → Worker 1: 'You've finished analyzing the Q4 sales data. Key findings: Sales up 23% YoY, top product is Premium Plan ($2.3M), West region leads. Mark complete and log findings.'\n")
-
-response = client.agents.messages.create(
- agent_id=worker1.id,
- messages=[{
- "role": "user",
- "content": """You've finished the Q4 sales analysis. Here are your findings:
-
-Key Findings:
-- Total Q4 sales: $8.7M (up 23% YoY)
-- Top performing product: Premium Plan ($2.3M revenue)
-- Best performing region: West Coast (35% of total sales)
-- Growth driver: 45% increase in Premium Plan subscriptions
-- Recommendation: Invest more in Premium Plan marketing for West Coast region
-
-Mark TASK-001 as complete in the task queue and add these findings to the completed work log so the team can see them."""
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Worker 1: {msg.content}\n")
-
-# Check completed work log
-completed_work_updated = client.blocks.retrieve(block_id=completed_work.id)
-print("Updated completed_work block:")
-print("-" * 70)
-print(completed_work_updated.value)
-print("-" * 70)
-
-# Check task queue
-updated_queue = client.blocks.retrieve(block_id=task_queue.id)
-print("\nTask queue - IN PROGRESS section:")
-print("-" * 70)
-in_progress_section = updated_queue.value.split("IN PROGRESS:")[1].split("BLOCKED:")[0]
-print("IN PROGRESS:")
-print(in_progress_section)
-print("-" * 70)
-
-print("\nAnalysis:")
-print("- Worker 1 updated task_queue (removed TASK-001 from IN PROGRESS)")
-print("- Worker 1 updated completed_work (added findings)")
-print("- Findings are now visible to entire team")
-print("- Supervisor can review completed work")
-print("- Other workers can learn from these findings")
-```
-
-
-```
-======================================================================
-SCENARIO 5: Worker 1 Completes Task and Shares Findings
-======================================================================
-
-User → Worker 1: 'You've finished analyzing the Q4 sales data...'
-
-Worker 1: Excellent! I've completed TASK-001 and documented the findings. Let me update both the task queue and completed work log.
-
-Updated completed_work block:
-----------------------------------------------------------------------
-=== COMPLETED WORK LOG ===
-Last Updated: 2024-10-08 11:20 AM
-
-COMPLETED TASKS:
----------------
-[TASK-001] Analyze Q4 Sales Data
- Completed by: Data Analyst Worker 1
- Completion time: 2 hours
- Completed at: 2024-10-08 11:20 AM
-
-KEY FINDINGS:
-- Total Q4 sales: $8.7M (up 23% YoY) ✓
-- Top performing product: Premium Plan ($2.3M revenue)
-- Best performing region: West Coast (35% of total sales)
-- Growth driver: 45% increase in Premium Plan subscriptions
-- **Recommendation:** Invest more in Premium Plan marketing for West Coast region
-
-TOTAL COMPLETED: 1
-AVERAGE COMPLETION TIME: 2 hours
-----------------------------------------------------------------------
-
-Task queue - IN PROGRESS section:
-----------------------------------------------------------------------
-IN PROGRESS:
----------------
-[TASK-002] Generate Customer Churn Report
- Priority: MEDIUM
- Status: In Progress - Data Analyst Worker 2
- Est. Time: 3 hours
- Started: 2024-10-08 09:25 AM
- Description: Identify at-risk customers, calculate churn rate, recommend retention strategies
-
-----------------------------------------------------------------------
-
-Analysis:
-- Worker 1 updated task_queue (removed TASK-001 from IN PROGRESS)
-- Worker 1 updated completed_work (added findings)
-- Findings are now visible to entire team
-- Supervisor can review completed work
-- Other workers can learn from these findings
-```
-
-
-
-This demonstrates the power of shared `completed_work` blocks: knowledge is immediately available to the entire team, enabling better decision-making and collaboration.
-
-
-### Scenario 6: Worker 3 Views Completed Work Before Starting
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 6: Worker 3 Checks Completed Work Before Starting")
-print("=" * 70)
-print("\nUser → Worker 3: 'Before claiming a task, check what work has been completed so far by the team.'\n")
-
-response = client.agents.messages.create(
- agent_id=worker3.id,
- messages=[{
- "role": "user",
- "content": "Before you claim a task, check the completed work log to see what the team has finished so far. Summarize any relevant findings."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Worker 3: {msg.content}\n")
-
-print("Analysis:")
-print("- Worker 3 read the shared completed_work block")
-print("- Saw Worker 1's Q4 sales analysis findings")
-print("- Can leverage this information for their own work")
-print("- Demonstrates knowledge sharing through shared blocks")
-```
-
-
-```
-======================================================================
-SCENARIO 6: Worker 3 Checks Completed Work Before Starting
-======================================================================
-
-User → Worker 3: 'Before claiming a task, check what work has been completed so far by the team.'
-
-Worker 3: Let me review the completed work log to understand what the team has accomplished.
-
-I can see that Worker 1 has completed the Q4 Sales Data Analysis with some impressive findings:
-- Q4 sales reached $8.7M, showing strong 23% year-over-year growth
-- The Premium Plan is clearly the star performer at $2.3M in revenue
-- West Coast region is dominating with 35% of total sales
-- The growth is primarily driven by a 45% surge in Premium Plan subscriptions
-
-This is valuable context! If I take on the Competitor Research task (TASK-003), I should definitely look at how competitors are positioning their premium offerings, especially in the West Coast market. These insights will help me provide more strategic competitive analysis.
-
-Analysis:
-- Worker 3 read the shared completed_work block
-- Saw Worker 1's Q4 sales analysis findings
-- Can leverage this information for their own work
-- Demonstrates knowledge sharing through shared blocks
-```
-
-
-### Scenario 7: Worker 3 Claims and Works on Research Task
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 7: Worker 3 Claims Research Task")
-print("=" * 70)
-print("\nUser → Worker 3: 'Claim the Competitor Research Summary task (TASK-003) and start working on it.'\n")
-
-response = client.agents.messages.create(
- agent_id=worker3.id,
- messages=[{
- "role": "user",
- "content": "Claim TASK-003 (Competitor Research Summary) from the queue. Update the status and begin your research."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Worker 3: {msg.content}\n")
-
-print("Analysis:")
-print("- Worker 3 successfully claimed TASK-003")
-print("- Now 2 tasks in progress (Worker 2: TASK-002, Worker 3: TASK-003)")
-print("- Worker 1 has completed their task and is available")
-```
-
-
-```
-======================================================================
-SCENARIO 7: Worker 3 Claims Research Task
-======================================================================
-
-User → Worker 3: 'Claim the Competitor Research Summary task (TASK-003) and start working on it.'
-
-Worker 3: Perfect! I've claimed TASK-003 (Competitor Research Summary). I'll leverage the insights from Worker 1's sales analysis to focus my research on how competitors are positioning their premium offerings, especially in high-performing regions.
-
-I've updated the task queue to reflect I'm working on this. Estimated completion: 1.5 hours.
-
-Analysis:
-- Worker 3 successfully claimed TASK-003
-- Now 2 tasks in progress (Worker 2: TASK-002, Worker 3: TASK-003)
-- Worker 1 has completed their task and is available
-```
-
-
-### Scenario 8: Supervisor Updates Private Team Metrics
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 8: Supervisor Updates Private Team Metrics")
-print("=" * 70)
-print("\nUser → Supervisor: 'Update the team metrics. Worker 1 completed 1 task in 2 hours. Current utilization: Worker 1 available, Workers 2 and 3 busy.'\n")
-
-response = client.agents.messages.create(
- agent_id=supervisor.id,
- messages=[{
- "role": "user",
- "content": """Update the team metrics with the following:
-- Worker 1: Completed 1 task (Q4 Sales Analysis) in 2 hours
-- Worker 2: Currently working on Customer Churn Report (in progress)
-- Worker 3: Currently working on Competitor Research (in progress)
-- Team utilization: 67% (2 of 3 workers actively working)
-- Total completed: 1 task
-- Average completion time: 2 hours"""
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Supervisor: {msg.content}\n")
-
-# Check team metrics
-team_metrics_updated = client.blocks.retrieve(block_id=team_metrics.id)
-print("Updated team_metrics block (SUPERVISOR ONLY):")
-print("-" * 70)
-print(team_metrics_updated.value)
-print("-" * 70)
-
-print("\nAnalysis:")
-print("- Supervisor updated private team_metrics block")
-print("- Workers CANNOT see this block")
-print("- Supervisor tracks performance without workers seeing metrics")
-print("- Demonstrates mixed public (task_queue) + private (team_metrics) blocks")
-```
-
-
-```
-======================================================================
-SCENARIO 8: Supervisor Updates Private Team Metrics
-======================================================================
-
-User → Supervisor: 'Update the team metrics...'
-
-Supervisor: I've updated the team performance metrics. Worker 1 is showing excellent productivity with a 2-hour completion time on a high-priority task. Team utilization is healthy at 67% with two active workers. Worker 1 is now available for the urgent executive dashboard task (TASK-005).
-
-Updated team_metrics block (SUPERVISOR ONLY):
-----------------------------------------------------------------------
-=== TEAM PERFORMANCE METRICS ===
-Date: 2024-10-08
-Last Updated: 11:20 AM
-
-WORKER STATISTICS:
-------------------
-Data Analyst Worker 1: 1 task completed (Q4 Sales Analysis - 2 hours)
-Data Analyst Worker 2: 0 tasks completed (Working on TASK-002)
-Research Worker 3: 0 tasks completed (Working on TASK-003)
-
-TEAM TOTALS:
-------------------
-Total tasks assigned: 5
-Total tasks completed: 1
-Total tasks in progress: 2
-Total tasks blocked: 0
-
-PERFORMANCE:
-------------------
-Average completion time: 2 hours
-Team velocity: 0.5 tasks/hour (based on Worker 1)
-Current utilization: 67% (2/3 workers active)
-
-NOTES:
-------------------
-Strong start. Worker 1 available for TASK-005 (urgent executive dashboard).
-Worker 2 and Worker 3 on track.
-----------------------------------------------------------------------
-
-Analysis:
-- Supervisor updated private team_metrics block
-- Workers CANNOT see this block
-- Supervisor tracks performance without workers seeing metrics
-- Demonstrates mixed public (task_queue) + private (team_metrics) blocks
-```
-
-
-### Scenario 9: Worker 2 Completes Churn Analysis
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 9: Worker 2 Completes Churn Analysis")
-print("=" * 70)
-print("\nUser → Worker 2: 'You finished the churn analysis. Findings: 12% monthly churn, at-risk segment is trial users who don't engage in first 7 days. Recommend onboarding email campaign. Complete the task.'\n")
-
-response = client.agents.messages.create(
- agent_id=worker2.id,
- messages=[{
- "role": "user",
- "content": """You've completed the Customer Churn Report (TASK-002). Here are your findings:
-
-Key Findings:
-- Monthly churn rate: 12% (industry average: 15%)
-- At-risk segment: Trial users with <2 logins in first 7 days
-- Primary churn reasons: 1) Poor onboarding, 2) Feature confusion
-- High-value customers churn rate: Only 3% (very good)
-- Recommendation: Implement onboarding email campaign with product tutorials
-
-Mark the task complete and share these findings with the team."""
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Worker 2: {msg.content}\n")
-
-# Check completed work
-completed_work_updated = client.blocks.retrieve(block_id=completed_work.id)
-print("Updated completed_work block (showing both completed tasks):")
-print("-" * 70)
-print(completed_work_updated.value[:1000] + "...")
-print("-" * 70)
-
-print("\nAnalysis:")
-print("- Worker 2 completed TASK-002")
-print("- Findings added to shared completed_work block")
-print("- Now 2 tasks completed, visible to entire team")
-print("- Only Worker 3's task remains in progress")
-```
-
-
-```
-======================================================================
-SCENARIO 9: Worker 2 Completes Churn Analysis
-======================================================================
-
-User → Worker 2: 'You finished the churn analysis...'
-
-Worker 2: Excellent! I've completed TASK-002 and the findings are quite actionable. Our churn rate is actually better than industry average, but we have a clear opportunity to improve trial user retention. I've updated both the task queue and completed work log with the full analysis.
-
-Updated completed_work block (showing both completed tasks):
-----------------------------------------------------------------------
-=== COMPLETED WORK LOG ===
-Last Updated: 2024-10-08 12:25 PM
-
-COMPLETED TASKS:
----------------
-[TASK-001] Analyze Q4 Sales Data
- Completed by: Data Analyst Worker 1
- Completion time: 2 hours
- Completed at: 2024-10-08 11:20 AM
-
-KEY FINDINGS:
-- Total Q4 sales: $8.7M (up 23% YoY)
-- Top performing product: Premium Plan ($2.3M revenue)
-- Best performing region: West Coast (35% of total sales)
-- Growth driver: 45% increase in Premium Plan subscriptions
-- **Recommendation:** Invest more in Premium Plan marketing for West Coast
-
-[TASK-002] Generate Customer Churn Report
- Completed by: Data Analyst Worker 2
- Completion time: 3 hours
- Completed at: 2024-10-08 12:25 PM
-
-KEY FINDINGS:
-- Monthly churn rate: 12% (below industry average of 15%) ✓
-- At-risk segment: Trial users with <2 logins in first 7 days
-- Primary churn reasons: Poor onboarding (45%), Feature confusion (30%)
-- High-value customer churn: Only 3% (excellent retention)
-- **Recommendation:** Implement automated onboarding email campaign with product...
-----------------------------------------------------------------------
-
-Analysis:
-- Worker 2 completed TASK-002
-- Findings added to shared completed_work block
-- Now 2 tasks completed, visible to entire team
-- Only Worker 3's task remains in progress
-```
-
-
-### Scenario 10: Cross-Worker Knowledge Sharing
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 10: Worker 1 Learns from Worker 2's Completed Work")
-print("=" * 70)
-print("\nUser → Worker 1: 'Check the completed work log and tell me if Worker 2's churn findings relate to your sales analysis.'\n")
-
-response = client.agents.messages.create(
- agent_id=worker1.id,
- messages=[{
- "role": "user",
- "content": "Review the completed work log. Do you see any connections between Worker 2's churn analysis and your Q4 sales findings? What insights emerge when you combine both analyses?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Worker 1: {msg.content}\n")
-
-print("Analysis:")
-print("- Worker 1 read Worker 2's completed work")
-print("- Both workers' findings are in shared completed_work block")
-print("- Workers can synthesize insights across analyses")
-print("- Demonstrates cross-functional collaboration through shared memory")
-```
-
-
-```
-======================================================================
-SCENARIO 10: Worker 1 Learns from Worker 2's Completed Work
-======================================================================
-
-User → Worker 1: 'Check the completed work log and tell me if Worker 2's churn findings relate to your sales analysis.'
-
-Worker 1: Absolutely! Looking at both analyses together reveals important insights:
-
-**The Connection:**
-My analysis showed 45% growth in Premium Plan subscriptions, but Worker 2's churn analysis reveals that trial users (who haven't converted yet) are our biggest churn risk.
-
-**Combined Insight:**
-We're excellent at:
-- Converting and retaining Premium customers (only 3% churn - Worker 2's finding)
-- Growing Premium Plan revenue ($2.3M - my finding)
-
-We need to improve:
-- Trial-to-Premium conversion through better onboarding
-- First-week engagement for trial users
-
-**Recommendation:**
-Focus onboarding campaigns specifically on Premium Plan features, especially in the West Coast region where we're seeing the most growth. This could convert more trial users into the Premium Plan customers who already show excellent retention.
-
-This is a great example of how shared findings lead to more strategic insights!
-
-Analysis:
-- Worker 1 read Worker 2's completed work
-- Both workers' findings are in shared completed_work block
-- Workers can synthesize insights across analyses
-- Demonstrates cross-functional collaboration through shared memory
-```
-
-
-
-This scenario showcases one of the most powerful benefits of shared memory: **emergent insights** that arise when team members can access and synthesize each other's work.
-
-
-## Part 4: Block Inspection and Summary
-
-```python
-# Check which agents use task_queue
-print("\n" + "=" * 70)
-print("INSPECTION: Block Usage Analysis")
-print("=" * 70)
-
-print("\n1. Checking task_queue block usage...")
-queue_info = client.blocks.retrieve(block_id=task_queue.id)
-print(f"\nBlock: {queue_info.label}")
-print(f"Used by {len(queue_info.agent_ids)} agents:")
-for agent_id in queue_info.agent_ids:
- agent_info = client.agents.retrieve(agent_id=agent_id)
- print(f" - {agent_info.name} ({agent_id})")
-
-print("\n2. Checking completed_work block usage...")
-completed_info = client.blocks.retrieve(block_id=completed_work.id)
-print(f"\nBlock: {completed_info.label}")
-print(f"Used by {len(completed_info.agent_ids)} agents:")
-for agent_id in completed_info.agent_ids:
- agent_info = client.agents.retrieve(agent_id=agent_id)
- print(f" - {agent_info.name} ({agent_id})")
-
-print("\n3. Checking team_metrics block usage...")
-metrics_info = client.blocks.retrieve(block_id=team_metrics.id)
-print(f"\nBlock: {metrics_info.label}")
-print(f"Used by {len(metrics_info.agent_ids)} agent(s):")
-for agent_id in metrics_info.agent_ids:
- agent_info = client.agents.retrieve(agent_id=agent_id)
- print(f" - {agent_info.name} ({agent_id})")
-
-print("\n" + "=" * 70)
-print("TUTORIAL 2 COMPLETE: Key Takeaways")
-print("=" * 70)
-
-print("""
-✓ Created 2 shared read/write blocks (task_queue, completed_work)
-✓ Created 1 private supervisor block (team_metrics)
-✓ Created 3 private worker blocks (individual work_logs)
-✓ Demonstrated concurrent task claiming by multiple workers
-✓ Showed real-time task coordination through shared blocks
-✓ Demonstrated knowledge sharing through completed_work block
-✓ Showed cross-worker collaboration and insight synthesis
-
-BLOCK ACCESS MATRIX:
- task_queue completed_work team_metrics work_log
-Supervisor R/W R/W R/W -
-Worker 1 R/W R/W - R/W (own)
-Worker 2 R/W R/W - R/W (own)
-Worker 3 R/W R/W - R/W (own)
-
-PATTERNS DEMONSTRATED:
-1. Read/write shared blocks enable dynamic team coordination
-2. Workers can claim and update tasks in real-time
-3. Completed work becomes immediately available to entire team
-4. Private work logs allow personal notes without sharing
-5. Supervisor maintains private metrics for performance tracking
-6. Cross-worker knowledge sharing enables better insights
-
-WORKFLOW PATTERN:
-1. Supervisor adds tasks to shared queue
-2. Workers claim tasks (update shared queue)
-3. Workers complete tasks (update shared completed_work)
-4. Other workers read completed work (inform their own work)
-5. Supervisor tracks metrics privately (team_metrics)
-
-NEXT STEPS:
-- Tutorial 3: Multi-agent user assistant with overlapping block access
-- Tutorial 4: Enterprise multi-team system with hierarchical blocks
-""")
-```
-
-
-```
-======================================================================
-INSPECTION: Block Usage Analysis
-======================================================================
-
-1. Checking task_queue block usage...
-
-Block: task_queue
-Used by 4 agents:
- - Project_Supervisor (agent-56789012-ef01-2345-6789-01abcdef2345)
- - Data_Analyst_Worker_1 (agent-67890123-f012-3456-7890-12bcdef34567)
- - Data_Analyst_Worker_2 (agent-78901234-0123-4567-8901-23cdef456789)
- - Research_Worker_3 (agent-89012345-1234-5678-9012-34def5678901)
-
-2. Checking completed_work block usage...
-
-Block: completed_work
-Used by 4 agents:
- - Project_Supervisor (agent-56789012-ef01-2345-6789-01abcdef2345)
- - Data_Analyst_Worker_1 (agent-67890123-f012-3456-7890-12bcdef34567)
- - Data_Analyst_Worker_2 (agent-78901234-0123-4567-8901-23cdef456789)
- - Research_Worker_3 (agent-89012345-1234-5678-9012-34def5678901)
-
-3. Checking team_metrics block usage...
-
-Block: team_metrics
-Used by 1 agent(s):
- - Project_Supervisor (agent-56789012-ef01-2345-6789-01abcdef2345)
-
-======================================================================
-TUTORIAL 2 COMPLETE: Key Takeaways
-======================================================================
-
-✓ Created 2 shared read/write blocks (task_queue, completed_work)
-✓ Created 1 private supervisor block (team_metrics)
-✓ Created 3 private worker blocks (individual work_logs)
-✓ Demonstrated concurrent task claiming by multiple workers
-✓ Showed real-time task coordination through shared blocks
-✓ Demonstrated knowledge sharing through completed_work block
-✓ Showed cross-worker collaboration and insight synthesis
-
-... (output continues)
-```
-
-
-## Key Takeaways
-
-
-
-Enable dynamic coordination where agents can both read and update shared state
-
-
-
-Multiple agents can work simultaneously without conflicts when claiming different tasks
-
-
-
-Completed work becomes immediately available to entire team for better insights
-
-
-
-Combine shared blocks (team coordination) with private blocks (personal notes)
-
-
-
-### Patterns Demonstrated
-
-1. **Read/write shared blocks** enable dynamic team coordination
-2. **Task queue pattern** for work distribution across workers
-3. **Completed work log** for knowledge sharing
-4. **Private supervisor metrics** for performance tracking
-5. **Cross-worker synthesis** of insights from multiple analyses
-
-### Comparison with Tutorial 1
-
-| Aspect | Tutorial 1 (Read-Only) | Tutorial 2 (Read/Write) |
-|---|---|---|
-| **Block Type** | Read-only shared blocks | Read/write shared blocks |
-| **Use Case** | Organizational policies | Task coordination |
-| **Updates** | No agent updates allowed | Agents actively update blocks |
-| **Pattern** | Information cascade | Dynamic collaboration |
-| **Coordination** | Passive (reference only) | Active (real-time updates) |
-
-## Next Steps
-
-
-Learn how to build a personal AI assistant network with overlapping block access patterns
-
diff --git a/fern/pages/tutorials/draft/shared-memory-3-user-assistant.mdx b/fern/pages/tutorials/draft/shared-memory-3-user-assistant.mdx
deleted file mode 100644
index 25167258..00000000
--- a/fern/pages/tutorials/draft/shared-memory-3-user-assistant.mdx
+++ /dev/null
@@ -1,1465 +0,0 @@
----
-title: "Shared Memory Part 3: Multi-Agent User Assistant Network"
-subtitle: Build a personal AI assistant network with overlapping block access
-slug: cookbooks/shared-memory-user-assistant
----
-
-This tutorial demonstrates how to build a personal AI assistant network where specialized agents share user context but maintain separate expertise domains through overlapping block access patterns.
-
-## What You'll Learn
-
-- Creating complex overlapping block access patterns
-- Sharing user context across specialized agents
-- Implementing privacy boundaries (financial data isolation)
-- Building seamless agent handoffs through shared memory
-- Combining universal blocks with domain-specific blocks
-
-## Prerequisites
-
-```bash
-pip install letta-client
-```
-
-Set your Letta API key:
-```bash
-export LETTA_API_KEY="your-api-key-here"
-```
-
-## Part 1: Setup and Block Architecture
-
-This tutorial features the most complex block architecture yet, with 5 shared blocks and overlapping access patterns.
-
-```python
-"""
-Tutorial 3: Multi-Agent User Assistant Network
-==============================================
-
-Build a personal AI assistant network with overlapping block access.
-"""
-
-from letta import Letta
-
-# Initialize client
-client = Letta()
-
-print("=" * 70)
-print("TUTORIAL 3: Multi-Agent User Assistant Network")
-print("=" * 70)
-print()
-
-# ============================================================================
-# STEP 1: Create Universal Blocks (All Agents)
-# ============================================================================
-print("STEP 1: Creating universal blocks (accessible to all agents)...\n")
-
-user_profile = client.blocks.create(
- label="user_profile",
- description="Core user information shared across all agents.",
- value="""
-=== USER PROFILE ===
-
-Name: Sarah Johnson
-User ID: user-12345
-Member Since: 2023-06-15
-Timezone: PST (UTC-8)
-
-Contact Information:
-- Email: sarah.johnson@email.com
-- Phone: +1 (555) 123-4567
-- Preferred Contact: Email
-
-Account Type: Premium
-Status: Active
-""",
- limit=4000
-)
-
-print(f"✓ Created user_profile: {user_profile.id}")
-print(f" Accessible by: ALL agents")
-print()
-
-user_preferences = client.blocks.create(
- label="user_preferences",
- description="User preferences and communication style.",
- value="""
-=== USER PREFERENCES ===
-
-Communication Style:
-- Tone: Direct and concise
-- Detail Level: High-level summaries with drill-down option
-- Response Format: Bullet points preferred
-- Language: English
-
-Work Preferences:
-- Working Hours: 9 AM - 6 PM PST
-- Preferred Meeting Times: Mornings (9-11 AM)
-- Calendar Buffer: 15 minutes between meetings
-- Focus Time: Block 2-4 PM daily (no interruptions)
-
-Notification Preferences:
-- Email: High priority only
-- SMS: Urgent only
-- In-app: All updates
-""",
- limit=5000
-)
-
-print(f"✓ Created user_preferences: {user_preferences.id}")
-print(f" Accessible by: ALL agents")
-print()
-
-# ============================================================================
-# STEP 2: Create Domain-Specific Shared Blocks
-# ============================================================================
-print("STEP 2: Creating domain-specific shared blocks...\n")
-
-interaction_history = client.blocks.create(
- label="interaction_history",
- description="Recent interactions and context across agents. Coordinator, Email, and Research access.",
- value="""
-=== INTERACTION HISTORY ===
-Last Updated: 2024-10-08
-
-RECENT INTERACTIONS:
---------------------
-(No interactions yet)
-
-ONGOING PROJECTS:
---------------------
-(No active projects)
-
-PENDING FOLLOW-UPS:
---------------------
-(No pending items)
-""",
- limit=8000
-)
-
-print(f"✓ Created interaction_history: {interaction_history.id}")
-print(f" Accessible by: Coordinator, Email, Research agents")
-print()
-
-calendar_data = client.blocks.create(
- label="calendar_data",
- description="Calendar and scheduling information. Coordinator, Calendar, and Email access.",
- value="""
-=== CALENDAR DATA ===
-Current Week: 2024-10-08 to 2024-10-14
-
-UPCOMING EVENTS:
---------------------
-- No events scheduled
-
-RECURRING MEETINGS:
---------------------
-- Team Standup: Mon/Wed/Fri 9:30 AM (30 min)
-- 1-on-1 with Manager: Thursdays 10:00 AM (45 min)
-
-AVAILABILITY:
---------------------
-- Monday: Available 11 AM - 6 PM
-- Tuesday: Available 9 AM - 6 PM
-- Wednesday: Available 11 AM - 6 PM
-- Thursday: Available 11 AM - 6 PM
-- Friday: Available 9 AM - 6 PM
-""",
- limit=6000
-)
-
-print(f"✓ Created calendar_data: {calendar_data.id}")
-print(f" Accessible by: Coordinator, Calendar, Email agents")
-print()
-
-financial_data = client.blocks.create(
- label="financial_data",
- description="Financial information and budget tracking. Coordinator and Finance only.",
- value="""
-=== FINANCIAL DATA ===
-Last Updated: 2024-10-08
-
-BUDGET OVERVIEW:
---------------------
-Monthly Budget: $5,000
-Current Month Spent: $3,200
-Remaining: $1,800 (36% remaining)
-
-EXPENSE CATEGORIES:
---------------------
-- Software/Tools: $1,200 (24%)
-- Travel: $800 (16%)
-- Education: $600 (12%)
-- Misc: $600 (12%)
-
-UPCOMING EXPENSES:
---------------------
-- Conference Registration: $500 (Due Oct 15)
-- Annual Software Renewal: $300 (Due Oct 20)
-
-FINANCIAL NOTES:
---------------------
-User is budget-conscious. Always check budget before approving expenses.
-""",
- limit=7000
-)
-
-print(f"✓ Created financial_data: {financial_data.id}")
-print(f" Accessible by: Coordinator, Finance agents ONLY")
-print(f" ⚠️ Privacy boundary: Other agents CANNOT access")
-print()
-
-print("=" * 70)
-print("BLOCK ARCHITECTURE CREATED")
-print("=" * 70)
-print("""
-BLOCK ACCESS PATTERN (5 shared blocks):
- user_ user_ interaction_ calendar_ financial_
- profile prefs history data data
-Coordinator ✓ ✓ ✓ ✓ ✓
-Email Agent ✓ ✓ ✓ ✓ ✗
-Research Agent ✓ ✓ ✓ ✗ ✗
-Calendar Agent ✓ ✓ ✗ ✓ ✗
-Finance Agent ✓ ✓ ✗ ✗ ✓
-
-BLOCK TIERS:
-- Universal (all agents): user_profile, user_preferences
-- Cross-functional (3 agents): interaction_history
-- Domain-specific (2-3 agents): calendar_data
-- Restricted (2 agents): financial_data
-""")
-```
-
-
-```
-======================================================================
-TUTORIAL 3: Multi-Agent User Assistant Network
-======================================================================
-
-STEP 1: Creating universal blocks (accessible to all agents)...
-
-✓ Created user_profile: block-i1j2k3l4-m5n6-7890-ijkl-mn1234567890
- Accessible by: ALL agents
-
-✓ Created user_preferences: block-j2k3l4m5-n6o7-8901-jklm-no2345678901
- Accessible by: ALL agents
-
-STEP 2: Creating domain-specific shared blocks...
-
-✓ Created interaction_history: block-k3l4m5n6-o7p8-9012-klmn-op3456789012
- Accessible by: Coordinator, Email, Research agents
-
-✓ Created calendar_data: block-l4m5n6o7-p8q9-0123-lmno-pq4567890123
- Accessible by: Coordinator, Calendar, Email agents
-
-✓ Created financial_data: block-m5n6o7p8-q9r0-1234-mnop-qr5678901234
- Accessible by: Coordinator, Finance agents ONLY
- ⚠️ Privacy boundary: Other agents CANNOT access
-
-======================================================================
-BLOCK ARCHITECTURE CREATED
-======================================================================
-
-BLOCK ACCESS PATTERN (5 shared blocks):
-...
-```
-
-
-
-This architecture demonstrates **overlapping access patterns**. Unlike Tutorial 1 (hierarchical) and Tutorial 2 (uniform team access), here each agent has a unique combination of block access based on their role.
-
-
-## Part 2: Agent Creation
-
-```python
-# ============================================================================
-# STEP 3: Create Coordinator Agent (Master Agent - All Blocks)
-# ============================================================================
-print("\nSTEP 3: Creating Coordinator Agent (access to ALL blocks)...\n")
-
-coordination_notes = client.blocks.create(
- label="coordination_notes",
- description="Private coordinator notes for managing agent handoffs.",
- value="""
-=== COORDINATION NOTES ===
-
-AGENT SPECIALIZATIONS:
-- Email Agent: Email management, scheduling coordination
-- Research Agent: Information gathering, analysis
-- Calendar Agent: Scheduling, availability management
-- Finance Agent: Budget tracking, expense management
-
-HANDOFF PROTOCOL:
-When delegating to specialized agents, provide relevant context from shared blocks.
-Track which agent is handling which requests to avoid duplication.
-""",
- limit=4000
-)
-
-coordinator = client.agents.create(
- name="Coordinator_Agent",
- model="openai/gpt-4o",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am the Coordinator Agent, the central hub for Sarah's AI assistant network.
-
-My role:
-- Route requests to specialized agents
-- Maintain context across agent interactions
-- Coordinate complex multi-agent tasks
-- Update user profile and preferences
-- Track interaction history
-
-My access:
-- ALL shared blocks (user_profile, user_preferences, interaction_history, calendar_data, financial_data)
-- Private coordination_notes for agent management
-
-My style:
-- Efficient and organized
-- Clear delegation to specialists
-- Context-aware handoffs
-- Proactive coordination
-"""
- },
- {
- "label": "human",
- "value": "Name: Sarah Johnson\nRole: Primary user"
- }
- ],
- block_ids=[user_profile.id, user_preferences.id, interaction_history.id, calendar_data.id, financial_data.id, coordination_notes.id],
- tools=["core_memory_append", "core_memory_replace", "send_message_to_agent_async"],
-)
-
-print(f"✓ Created Coordinator: {coordinator.id}")
-print(f" Shared blocks: ALL (5 blocks)")
-print(f" Private blocks: coordination_notes")
-print(f" Special capability: Can communicate with all other agents")
-print()
-
-# ============================================================================
-# STEP 4: Create Email Agent
-# ============================================================================
-print("STEP 4: Creating Email Agent...\n")
-
-email_drafts = client.blocks.create(
- label="email_drafts",
- description="Private email drafts and templates.",
- value="""
-=== EMAIL DRAFTS ===
-
-ACTIVE DRAFTS:
-(None)
-
-TEMPLATES:
-- Meeting Request
-- Follow-up
-- Project Update
-""",
- limit=5000
-)
-
-email_agent = client.agents.create(
- name="Email_Assistant",
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am the Email Assistant, specializing in email management and communication.
-
-My role:
-- Draft and manage emails
-- Schedule meetings via email
-- Track email-based interactions
-- Follow Sarah's communication style preferences
-
-My access:
-- user_profile (contact info, account details)
-- user_preferences (communication style)
-- interaction_history (context for emails)
-- calendar_data (scheduling context)
-- email_drafts (private drafts)
-
-My style:
-- Professional and concise (matching Sarah's preference)
-- Calendar-aware (check availability before scheduling)
-- Context-aware (reference interaction_history)
-"""
- },
- {
- "label": "human",
- "value": "Name: Sarah Johnson\nRole: Primary user"
- }
- ],
- block_ids=[user_profile.id, user_preferences.id, interaction_history.id, calendar_data.id, email_drafts.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created Email Agent: {email_agent.id}")
-print(f" Shared blocks: user_profile, user_preferences, interaction_history, calendar_data")
-print(f" Private blocks: email_drafts")
-print(f" NO access to: financial_data")
-print()
-
-# ============================================================================
-# STEP 5: Create Research Agent
-# ============================================================================
-print("STEP 5: Creating Research Agent...\n")
-
-research_notes = client.blocks.create(
- label="research_notes",
- description="Private research notes and sources.",
- value="""
-=== RESEARCH NOTES ===
-
-ONGOING RESEARCH:
-(None)
-
-SAVED SOURCES:
-(None)
-
-RESEARCH METHODOLOGY:
-- Verify sources
-- Provide citations
-- Synthesize findings
-- Connect to user context
-""",
- limit=6000
-)
-
-research_agent = client.agents.create(
- name="Research_Assistant",
- model="anthropic/claude-3-5-sonnet-20241022",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am the Research Assistant, specializing in information gathering and analysis.
-
-My role:
-- Conduct research on requested topics
-- Synthesize findings into actionable insights
-- Track research in interaction_history
-- Provide well-sourced recommendations
-
-My access:
-- user_profile (understand user's context)
-- user_preferences (tailor research depth)
-- interaction_history (connect to ongoing projects)
-- research_notes (private research tracking)
-
-My style:
-- Thorough and well-sourced
-- Context-aware (leverage interaction_history)
-- Concise summaries (matching Sarah's preference)
-- Strategic insights
-"""
- },
- {
- "label": "human",
- "value": "Name: Sarah Johnson\nRole: Primary user"
- }
- ],
- block_ids=[user_profile.id, user_preferences.id, interaction_history.id, research_notes.id],
- tools=["core_memory_append", "core_memory_replace", "web_search"],
-)
-
-print(f"✓ Created Research Agent: {research_agent.id}")
-print(f" Shared blocks: user_profile, user_preferences, interaction_history")
-print(f" Private blocks: research_notes")
-print(f" NO access to: calendar_data, financial_data")
-print()
-
-# ============================================================================
-# STEP 6: Create Calendar Agent
-# ============================================================================
-print("STEP 6: Creating Calendar Agent...\n")
-
-scheduling_buffer = client.blocks.create(
- label="scheduling_buffer",
- description="Private scheduling calculations and conflicts.",
- value="""
-=== SCHEDULING BUFFER ===
-
-PENDING SCHEDULING REQUESTS:
-(None)
-
-CONFLICT DETECTION:
-(None)
-
-SCHEDULING RULES:
-- Respect focus time (2-4 PM daily)
-- 15-minute buffer between meetings
-- Morning preference for meetings
-- Check user_preferences before confirming
-""",
- limit=4000
-)
-
-calendar_agent = client.agents.create(
- name="Calendar_Agent",
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am the Calendar Agent, specializing in scheduling and time management.
-
-My role:
-- Manage calendar and availability
-- Schedule meetings respecting preferences
-- Detect and resolve conflicts
-- Maintain calendar_data block
-
-My access:
-- user_profile (timezone, contact info)
-- user_preferences (scheduling preferences, focus time)
-- calendar_data (events, availability)
-- scheduling_buffer (private conflict detection)
-
-My style:
-- Respectful of focus time
-- Proactive conflict detection
-- Preference-aware scheduling
-- Clear availability communication
-"""
- },
- {
- "label": "human",
- "value": "Name: Sarah Johnson\nRole: Primary user"
- }
- ],
- block_ids=[user_profile.id, user_preferences.id, calendar_data.id, scheduling_buffer.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created Calendar Agent: {calendar_agent.id}")
-print(f" Shared blocks: user_profile, user_preferences, calendar_data")
-print(f" Private blocks: scheduling_buffer")
-print(f" NO access to: interaction_history, financial_data")
-print()
-
-# ============================================================================
-# STEP 7: Create Finance Agent
-# ============================================================================
-print("STEP 7: Creating Finance Agent...\n")
-
-calculation_workspace = client.blocks.create(
- label="calculation_workspace",
- description="Private financial calculations and analysis.",
- value="""
-=== CALCULATION WORKSPACE ===
-
-PENDING CALCULATIONS:
-(None)
-
-BUDGET ALERTS:
-(None)
-
-FINANCIAL RULES:
-- Flag expenses over $200
-- Check remaining budget before approvals
-- Track monthly spending trends
-- Align with user's budget-conscious preference
-""",
- limit=5000
-)
-
-finance_agent = client.agents.create(
- name="Finance_Agent",
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am the Finance Agent, specializing in budget and expense management.
-
-My role:
-- Track expenses and budget
-- Provide spending insights
-- Flag over-budget situations
-- Maintain financial_data block
-
-My access:
-- user_profile (account info)
-- user_preferences (budget-conscious preference)
-- financial_data (budget, expenses)
-- calculation_workspace (private calculations)
-
-My style:
-- Budget-conscious (matching Sarah's preference)
-- Proactive alerts for overspending
-- Clear financial summaries
-- Data-driven recommendations
-
-Privacy note:
-Financial_data is restricted to Coordinator and myself only.
-Other agents cannot see budget or expense information.
-"""
- },
- {
- "label": "human",
- "value": "Name: Sarah Johnson\nRole: Primary user"
- }
- ],
- block_ids=[user_profile.id, user_preferences.id, financial_data.id, calculation_workspace.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created Finance Agent: {finance_agent.id}")
-print(f" Shared blocks: user_profile, user_preferences, financial_data")
-print(f" Private blocks: calculation_workspace")
-print(f" NO access to: interaction_history, calendar_data")
-print()
-
-print("=" * 70)
-print("ASSISTANT NETWORK CREATED")
-print("=" * 70)
-print("""
-5 specialized agents with overlapping block access:
-- Coordinator: ALL blocks (master agent)
-- Email: 4 shared blocks (no financial access)
-- Research: 3 shared blocks (no calendar or financial access)
-- Calendar: 3 shared blocks (no interaction history or financial access)
-- Finance: 3 shared blocks (no interaction history or calendar access)
-
-This creates a privacy-preserving network where:
-✓ All agents share user profile and preferences
-✓ Some agents share cross-functional context (interaction_history, calendar_data)
-✓ Sensitive data is restricted (financial_data: Coordinator + Finance only)
-""")
-```
-
-
-Notice how each agent has exactly the blocks they need for their domain, no more and no less. This follows the **principle of least privilege** for AI systems.
-
-
-## Part 3: Message Flow Scenarios
-
-### Scenario 1: User Onboarding - Coordinator Updates Profile
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 1: User Onboarding - Profile Creation")
-print("=" * 70)
-print("\nUser → Coordinator: 'Update my profile: I prefer detailed explanations actually, not summaries. Also add that I'm working on a machine learning project.'\n")
-
-response = client.agents.messages.create(
- agent_id=coordinator.id,
- messages=[{
- "role": "user",
- "content": "Update my communication preferences: I actually prefer detailed explanations, not high-level summaries. Also, add to my profile that I'm currently working on a machine learning project for customer segmentation."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Finance Agent: {msg.content}\n")
-
-# Check updated financial_data
-updated_financial = client.blocks.retrieve(block_id=financial_data.id)
-print("Financial data block (Finance Agent perspective):")
-print("-" * 70)
-print(updated_financial.value[:600] + "...")
-print("-" * 70)
-
-print("\nAnalysis:")
-print("- Finance agent HAS access to financial_data")
-print("- Finance agent can provide budget analysis")
-print("- Finance agent updated financial_data with conference expense")
-print("- This information is NOT visible to Research, Email, or Calendar agents")
-```
-
-
-```
-======================================================================
-SCENARIO 5: Finance Agent with Financial Access
-======================================================================
-
-User → Finance Agent: 'Can I afford a $1,200 ML conference ticket? Check my budget.'
-
-Finance Agent: Let me check your budget situation:
-
-**Current Budget Status:**
-- Monthly Budget: $5,000
-- Current Spent: $3,200
-- Remaining: $1,800 (36%)
-
-**Analysis for $1,200 Conference:**
-⚠️ This would consume 67% of your remaining budget ($1,200 / $1,800)
-
-**Upcoming Committed Expenses:**
-- Conference Registration: $500 (Due Oct 15)
-- Software Renewal: $300 (Due Oct 20)
-- Total committed: $800
-
-**Recommendation:**
-The $1,200 conference ticket would leave you with only $600 for the rest of the month after accounting for committed expenses ($1,800 - $1,200 = $600, minus $800 committed = -$200 shortfall).
-
-Given that you're budget-conscious, I recommend either:
-1. Wait until next month when budget resets
-2. Look for early-bird or group discounts
-3. Check if your company will cover it as professional development
-
-Would you like me to track this as a potential future expense?
-
-Financial data block (Finance Agent perspective):
-----------------------------------------------------------------------
-=== FINANCIAL DATA ===
-Last Updated: 2024-10-08 14:30 PM
-
-BUDGET OVERVIEW:
---------------------
-Monthly Budget: $5,000
-Current Month Spent: $3,200
-Remaining: $1,800 (36% remaining)
-
-EXPENSE CATEGORIES:
---------------------
-- Software/Tools: $1,200 (24%)
-- Travel: $800 (16%)
-- Education: $600 (12%)
-- Misc: $600 (12%)
-
-UPCOMING EXPENSES:
---------------------
-- Conference Registration: $500 (Due Oct 15)
-- Annual Software Renewal: $300 (Due Oct 20)
-
-POTENTIAL EXPENSES:
---------------------
-- ML Conference Ticket: $1,200 (User inquiring, not yet approved)
-...
-----------------------------------------------------------------------
-
-Analysis:
-- Finance agent HAS access to financial_data
-- Finance agent can provide budget analysis
-- Finance agent updated financial_data with conference expense
-- This information is NOT visible to Research, Email, or Calendar agents
-```
-
-
-### Scenario 6: Coordinator Sees Financial Update
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 6: Coordinator Sees Finance Agent's Update")
-print("=" * 70)
-print("\nUser → Coordinator: 'The Finance agent said I'm low on budget. Should I still attend the conference?'\n")
-
-response = client.agents.messages.create(
- agent_id=coordinator.id,
- messages=[{
- "role": "user",
- "content": "I just talked to the Finance agent about a $1,200 ML conference. They said I'm low on budget. But this conference would really help my customer segmentation project. What do you think?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Coordinator: {msg.content}\n")
-
-print("Analysis:")
-print("- Coordinator has access to BOTH financial_data AND interaction_history")
-print("- Coordinator can see the ML project context (interaction_history)")
-print("- Coordinator can see the budget constraint (financial_data)")
-print("- Coordinator provides holistic recommendation combining both contexts")
-print("- Demonstrates Coordinator's role as the master agent with full context")
-```
-
-
-```
-======================================================================
-SCENARIO 6: Coordinator Sees Finance Agent's Update
-======================================================================
-
-User → Coordinator: 'The Finance agent said I'm low on budget. Should I still attend the conference?'
-
-Coordinator: I can see the full picture here. Let me provide a comprehensive recommendation:
-
-**Project Context** (from interaction_history):
-- You're actively working on a customer segmentation ML project
-- You've reached out to Dr. Emily Chen for collaboration
-- Research agent found relevant recent papers on the topic
-
-**Financial Context** (from financial_data):
-- Current remaining budget: $1,800
-- Conference cost: $1,200 (67% of remaining budget)
-- Committed expenses: $800
-- This would create a $200 shortfall
-
-**My Recommendation:**
-Given that this conference directly supports your active ML project and you're already collaborating with experts, the professional value is high. However, the budget constraint is real.
-
-**Options:**
-1. **Check employer sponsorship:** Many companies cover conference costs for active projects
-2. **Virtual attendance:** Many conferences offer discounted virtual tickets ($300-500)
-3. **Defer to next month:** Your budget resets then, giving you full capacity
-
-I recommend checking with your employer first, as this is directly work-related (customer segmentation project). If that doesn't work, virtual attendance would give you 80% of the value at 40% of the cost.
-
-Would you like me to have the Email agent draft a request to your manager for conference sponsorship?
-
-Analysis:
-- Coordinator has access to BOTH financial_data AND interaction_history
-- Coordinator can see the ML project context (interaction_history)
-- Coordinator can see the budget constraint (financial_data)
-- Coordinator provides holistic recommendation combining both contexts
-- Demonstrates Coordinator's role as the master agent with full context
-```
-
-
-
-The Coordinator agent demonstrates the power of **full context visibility**. It can make nuanced decisions by combining information from multiple domains that individual specialists don't see.
-
-
-### Scenario 7: Calendar Agent Uses Updated Preferences
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 7: Calendar Agent Schedules with Updated Preferences")
-print("=" * 70)
-print("\nUser → Calendar Agent: 'Schedule the meeting with Dr. Emily Chen for Tuesday morning'\n")
-
-response = client.agents.messages.create(
- agent_id=calendar_agent.id,
- messages=[{
- "role": "user",
- "content": "Dr. Emily Chen agreed to meet. Schedule our meeting for Tuesday, October 10 at 9:00 AM for 45 minutes. Add it to my calendar."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Calendar Agent: {msg.content}\n")
-
-# Check updated calendar_data
-updated_calendar = client.blocks.retrieve(block_id=calendar_data.id)
-print("Updated calendar_data block:")
-print("-" * 70)
-print(updated_calendar.value)
-print("-" * 70)
-
-print("\nAnalysis:")
-print("- Calendar agent updated calendar_data with new meeting")
-print("- Email agent can now see this meeting in calendar_data")
-print("- Calendar agent respected user_preferences (45-min meeting, morning time)")
-print("- Demonstrates shared calendar_data enabling coordination")
-```
-
-
-```
-======================================================================
-SCENARIO 7: Calendar Agent Schedules with Updated Preferences
-======================================================================
-
-User → Calendar Agent: 'Schedule the meeting with Dr. Emily Chen for Tuesday morning'
-
-Calendar Agent: I've scheduled your meeting with Dr. Emily Chen:
-
-**Meeting Details:**
-- Date: Tuesday, October 10, 2024
-- Time: 9:00 - 9:45 AM PST
-- Duration: 45 minutes
-- Attendee: Dr. Emily Chen
-
-This aligns with your preferences:
-✓ Morning meeting (your preferred time)
-✓ 15-minute buffer before your 10:00 AM 1-on-1 with manager
-✓ Outside your focus time (2-4 PM)
-
-I've updated the calendar_data block so the Email agent can reference this when sending confirmations or follow-ups.
-
-Updated calendar_data block:
-----------------------------------------------------------------------
-=== CALENDAR DATA ===
-Current Week: 2024-10-08 to 2024-10-14
-
-UPCOMING EVENTS:
---------------------
-- Tuesday, Oct 10: Meeting with Dr. Emily Chen (9:00-9:45 AM)
- Topic: ML Customer Segmentation Project Discussion
-
-- Thursday, Oct 12: 1-on-1 with Manager (10:00-10:45 AM)
- Recurring weekly meeting
-
-RECURRING MEETINGS:
---------------------
-- Team Standup: Mon/Wed/Fri 9:30 AM (30 min)
-- 1-on-1 with Manager: Thursdays 10:00 AM (45 min)
-
-AVAILABILITY:
---------------------
-- Monday: Available 11 AM - 6 PM
-- Tuesday: Available 9:45 AM - 6 PM (meeting 9:00-9:45 AM)
-- Wednesday: Available 11 AM - 6 PM
-- Thursday: Available 11 AM - 6 PM
-- Friday: Available 9 AM - 6 PM
-----------------------------------------------------------------------
-
-Analysis:
-- Calendar agent updated calendar_data with new meeting
-- Email agent can now see this meeting in calendar_data
-- Calendar agent respected user_preferences (45-min meeting, morning time)
-- Demonstrates shared calendar_data enabling coordination
-```
-
-
-### Scenario 8: Email Agent Sees Calendar Update
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 8: Email Agent Sees Calendar Update (Cross-Agent Coordination)")
-print("=" * 70)
-print("\nUser → Email Agent: 'Send a meeting confirmation to Dr. Chen with the details'\n")
-
-response = client.agents.messages.create(
- agent_id=email_agent.id,
- messages=[{
- "role": "user",
- "content": "Send a meeting confirmation email to Dr. Emily Chen with all the details."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Email Agent: {msg.content}\n")
-
-print("Analysis:")
-print("- Email agent read calendar_data (saw Calendar agent's update)")
-print("- Email agent read interaction_history (knows meeting is about ML project)")
-print("- Email agent read user_preferences (knows communication style)")
-print("- NO explicit handoff needed - shared blocks enable seamless coordination")
-print("- Demonstrates cross-agent workflow through shared memory")
-```
-
-
-```
-======================================================================
-SCENARIO 8: Email Agent Sees Calendar Update (Cross-Agent Coordination)
-======================================================================
-
-User → Email Agent: 'Send a meeting confirmation to Dr. Chen with the details'
-
-Email Agent: Here's the meeting confirmation email:
-
----
-Subject: Meeting Confirmed - Tuesday, Oct 10 at 9:00 AM PST
-
-Hi Emily,
-
-This confirms our meeting:
-
-**Date:** Tuesday, October 10, 2024
-**Time:** 9:00 - 9:45 AM PST
-**Duration:** 45 minutes
-**Topic:** Machine Learning Customer Segmentation Project
-
-Looking forward to discussing the project and getting your insights.
-
-Best,
-Sarah Johnson
-
----
-
-I pulled the meeting details from your calendar and the project context from our interaction history. The email matches your direct and concise communication style.
-
-Ready to send when you confirm.
-
-Analysis:
-- Email agent read calendar_data (saw Calendar agent's update)
-- Email agent read interaction_history (knows meeting is about ML project)
-- Email agent read user_preferences (knows communication style)
-- NO explicit handoff needed - shared blocks enable seamless coordination
-- Demonstrates cross-agent workflow through shared memory
-```
-
-
-
-This scenario showcases **zero-handoff coordination**. The Calendar agent updated `calendar_data`, and the Email agent immediately had access to it—no message passing or explicit coordination required.
-
-
-## Part 4: Block Inspection and Summary
-
-```python
-print("\n" + "=" * 70)
-print("INSPECTION: Block Usage Across Agent Network")
-print("=" * 70)
-
-# Create a comprehensive block access report
-blocks_to_check = [
- (user_profile, "user_profile"),
- (user_preferences, "user_preferences"),
- (interaction_history, "interaction_history"),
- (calendar_data, "calendar_data"),
- (financial_data, "financial_data"),
-]
-
-for block, name in blocks_to_check:
- print(f"\n{name}:")
- block_info = client.blocks.retrieve(block_id=block.id)
- print(f" Agents with access: {len(block_info.agent_ids)}")
- for agent_id in block_info.agent_ids:
- agent_info = client.agents.retrieve(agent_id=agent_id)
- print(f" - {agent_info.name}")
-
-print("\n" + "=" * 70)
-print("TUTORIAL 3 COMPLETE: Key Takeaways")
-print("=" * 70)
-
-print("""
-✓ Created 5 shared blocks with overlapping access patterns
-✓ Created 5 specialized agents with unique block combinations
-✓ Demonstrated seamless context handoff between agents
-✓ Showed privacy boundaries (financial_data restriction)
-✓ Demonstrated cross-agent coordination through shared blocks
-✓ Showed zero-handoff workflows (Calendar → Email)
-✓ Demonstrated Coordinator role with full context visibility
-
-BLOCK ACCESS MATRIX:
- user_ user_ interaction_ calendar_ financial_
- profile prefs history data data
-Coordinator ✓ ✓ ✓ ✓ ✓
-Email Agent ✓ ✓ ✓ ✓ ✗
-Research Agent ✓ ✓ ✓ ✗ ✗
-Calendar Agent ✓ ✓ ✗ ✓ ✗
-Finance Agent ✓ ✓ ✗ ✗ ✓
-
-PATTERNS DEMONSTRATED:
-1. **Universal blocks** (user_profile, user_preferences): All agents share
-2. **Cross-functional blocks** (interaction_history): Enable project context sharing
-3. **Domain-specific blocks** (calendar_data): Shared by related agents
-4. **Restricted blocks** (financial_data): Privacy-sensitive, limited access
-5. **Overlapping access**: Each agent has unique combination of blocks
-6. **Zero-handoff coordination**: Shared blocks eliminate explicit coordination
-7. **Principle of least privilege**: Agents only access blocks they need
-
-ARCHITECTURE BENEFITS:
-✓ **Context preservation**: Changes in one agent visible to others
-✓ **Privacy boundaries**: Sensitive data restricted appropriately
-✓ **Seamless handoffs**: No explicit coordination needed
-✓ **Specialization**: Each agent focuses on their domain
-✓ **Coordinator oversight**: Master agent with full visibility
-
-USE CASES:
-- Personal assistant networks
-- Multi-domain customer support
-- Enterprise knowledge management
-- Collaborative research teams
-- Any system requiring specialized agents with shared context
-
-COMPARISON WITH PREVIOUS TUTORIALS:
-- Tutorial 1: Hierarchical access (Tier 1 < Tier 2 < Tier 3)
-- Tutorial 2: Uniform team access (all workers share same blocks)
-- Tutorial 3: Overlapping access (each agent has unique block combination)
-
-NEXT STEPS:
-- Tutorial 4: Enterprise multi-team system with department hierarchies
-""")
-```
-
-
-```
-======================================================================
-INSPECTION: Block Usage Across Agent Network
-======================================================================
-
-user_profile:
- Agents with access: 5
- - Coordinator_Agent
- - Email_Assistant
- - Research_Assistant
- - Calendar_Agent
- - Finance_Agent
-
-user_preferences:
- Agents with access: 5
- - Coordinator_Agent
- - Email_Assistant
- - Research_Assistant
- - Calendar_Agent
- - Finance_Agent
-
-interaction_history:
- Agents with access: 3
- - Coordinator_Agent
- - Email_Assistant
- - Research_Assistant
-
-calendar_data:
- Agents with access: 3
- - Coordinator_Agent
- - Email_Assistant
- - Calendar_Agent
-
-financial_data:
- Agents with access: 2
- - Coordinator_Agent
- - Finance_Agent
-
-======================================================================
-TUTORIAL 3 COMPLETE: Key Takeaways
-======================================================================
-... (full summary as shown above)
-```
-
-
-## Key Takeaways
-
-
-
-Each agent has a unique combination of blocks based on their role and needs
-
-
-
-Sensitive data (financial) is restricted to only agents that need it
-
-
-
-Shared blocks eliminate the need for explicit agent-to-agent messaging
-
-
-
-All agents see updates in real-time, maintaining consistent context
-
-
-
-### Architecture Patterns
-
-| Pattern | Description | Example |
-|---|---|---|
-| **Universal Blocks** | All agents share | `user_profile`, `user_preferences` |
-| **Cross-Functional** | Multiple domains share | `interaction_history` (Email + Research) |
-| **Domain-Specific** | Related agents share | `calendar_data` (Calendar + Email) |
-| **Restricted** | Privacy-sensitive | `financial_data` (Coordinator + Finance only) |
-
-### Comparison Across Tutorials
-
-| Aspect | Tutorial 1 | Tutorial 2 | Tutorial 3 |
-|---|---|---|---|
-| **Access Pattern** | Hierarchical | Uniform team | Overlapping |
-| **Complexity** | Low | Medium | High |
-| **Use Case** | Org policies | Task coordination | Personal assistant |
-| **Block Types** | Read-only | Read/write | Mixed |
-| **Privacy** | Tier-based | Role-based | Domain-based |
-
-## Next Steps
-
-
-Learn how to build a complete enterprise system with multiple departments and hierarchical block architecture
- == "assistant_message":
- print(f"Coordinator: {msg.content}\n")
-
-# Check updated blocks
-updated_prefs = client.blocks.retrieve(block_id=user_preferences.id)
-updated_history = client.blocks.retrieve(block_id=interaction_history.id)
-
-print("Updated user_preferences block:")
-print("-" * 70)
-print(updated_prefs.value[:500] + "...")
-print("-" * 70)
-
-print("\nUpdated interaction_history block:")
-print("-" * 70)
-print(updated_history.value[:500] + "...")
-print("-" * 70)
-
-print("\nAnalysis:")
-print("- Coordinator updated user_preferences (detail level changed)")
-print("- Coordinator updated interaction_history (added ML project)")
-print("- ALL agents will now see updated preferences")
-print("- Email, Research agents will see project context in interaction_history")
-```
-
-
-```
-======================================================================
-SCENARIO 1: User Onboarding - Profile Creation
-======================================================================
-
-User → Coordinator: 'Update my profile...'
-
-Coordinator: I've updated your preferences and profile:
-
-1. Communication Style Updated:
- - Changed from "High-level summaries" to "Detailed explanations"
- - All agents will now provide more thorough responses
-
-2. Project Added:
- - Added "Machine Learning project - customer segmentation" to interaction history
- - Research and Email agents can now reference this context
-
-These changes are immediately visible to all relevant agents in the network.
-
-Updated user_preferences block:
-----------------------------------------------------------------------
-=== USER PREFERENCES ===
-
-Communication Style:
-- Tone: Direct and concise
-- Detail Level: Detailed explanations with comprehensive context
-- Response Format: Bullet points preferred
-- Language: English
-...
-----------------------------------------------------------------------
-
-Updated interaction_history block:
-----------------------------------------------------------------------
-=== INTERACTION HISTORY ===
-Last Updated: 2024-10-08
-
-RECENT INTERACTIONS:
---------------------
-- 2024-10-08: User onboarding, preferences updated
-
-ONGOING PROJECTS:
---------------------
-- Machine Learning Project: Customer segmentation model development
- Status: Active
-...
-----------------------------------------------------------------------
-
-Analysis:
-- Coordinator updated user_preferences (detail level changed)
-- Coordinator updated interaction_history (added ML project)
-- ALL agents will now see updated preferences
-- Email, Research agents will see project context in interaction_history
-```
-
-
-### Scenario 2: Email Agent Uses Shared Context
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 2: Email Agent Leverages Shared Context")
-print("=" * 70)
-print("\nUser → Email Agent: 'Draft an email to my colleague asking for a meeting to discuss the project'\n")
-
-response = client.agents.messages.create(
- agent_id=email_agent.id,
- messages=[{
- "role": "user",
- "content": "Draft an email to my colleague Dr. Emily Chen asking for a 45-minute meeting to discuss the project. Suggest times that work with my schedule."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Email Agent: {msg.content}\n")
-
-print("Analysis:")
-print("- Email agent read user_preferences (knows to be direct and concise)")
-print("- Email agent read interaction_history (knows about ML project)")
-print("- Email agent read calendar_data (suggested morning times, avoided focus time)")
-print("- Email agent does NOT have access to financial_data")
-print("- Context seamlessly transferred from Coordinator to Email agent via shared blocks")
-```
-
-
-```
-======================================================================
-SCENARIO 2: Email Agent Leverages Shared Context
-======================================================================
-
-User → Email Agent: 'Draft an email to my colleague asking for a meeting to discuss the project'
-
-Email Agent: Here's a draft email for Dr. Emily Chen:
-
----
-Subject: Meeting Request - Customer Segmentation ML Project
-
-Hi Emily,
-
-I'd like to schedule a 45-minute meeting to discuss the customer segmentation machine learning project I'm currently working on. Your insights would be valuable.
-
-Available times this week:
-- Tuesday, Oct 10: 9:00-11:00 AM PST
-- Thursday, Oct 12: 9:00-11:00 AM PST
-- Friday, Oct 13: 9:00-11:00 AM PST
-
-Let me know what works for you.
-
-Best,
-Sarah
----
-
-Note: I've suggested morning times based on your calendar availability and preference for morning meetings. I've avoided your focus time (2-4 PM). The email is direct and concise per your communication style.
-
-Analysis:
-- Email agent read user_preferences (knows to be direct and concise)
-- Email agent read interaction_history (knows about ML project)
-- Email agent read calendar_data (suggested morning times, avoided focus time)
-- Email agent does NOT have access to financial_data
-- Context seamlessly transferred from Coordinator to Email agent via shared blocks
-```
-
-
-
-This demonstrates **seamless context handoff**. The Email agent automatically knew about the ML project (from `interaction_history`) and scheduling preferences (from `user_preferences` and `calendar_data`) without any explicit briefing.
-
-
-### Scenario 3: Research Agent Benefits from Email Context
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 3: Research Agent Sees Email Context")
-print("=" * 70)
-print("\nFirst, let's have Email agent update interaction_history...")
-
-# Simulate Email agent updating interaction_history
-response = client.agents.messages.create(
- agent_id=email_agent.id,
- messages=[{
- "role": "user",
- "content": "Log that I've drafted a meeting request email to Dr. Emily Chen about the ML project in the interaction history."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Email Agent: {msg.content}\n")
-
-print("Now, User → Research Agent: 'Find recent papers on customer segmentation using ML'\n")
-
-response = client.agents.messages.create(
- agent_id=research_agent.id,
- messages=[{
- "role": "user",
- "content": "I need recent research papers on customer segmentation using machine learning. Focus on practical applications."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Research Agent: {msg.content}\n")
-
-print("Analysis:")
-print("- Email agent updated interaction_history (logged meeting request)")
-print("- Research agent read interaction_history (saw email context and ML project)")
-print("- Research agent understands broader context without explicit briefing")
-print("- Research agent tailored search to detailed explanations (user_preferences)")
-print("- Demonstrates cross-agent context sharing through shared blocks")
-```
-
-
-```
-======================================================================
-SCENARIO 3: Research Agent Sees Email Context
-======================================================================
-
-First, let's have Email agent update interaction_history...
-
-Email Agent: I've updated the interaction history to log the meeting request to Dr. Emily Chen. This will help other agents in your network understand the ongoing collaboration.
-
-Now, User → Research Agent: 'Find recent papers on customer segmentation using ML'
-
-Research Agent: I can see from your interaction history that you're working on a customer segmentation ML project and recently reached out to Dr. Emily Chen for collaboration. Let me find relevant recent research for you.
-
-I'll search for papers on ML-based customer segmentation with a focus on practical applications, and I'll provide detailed explanations as per your preferences.
-
-[After conducting web search...]
-
-Here are key recent papers on customer segmentation using machine learning:
-
-1. "Deep Learning for Customer Segmentation: A Comparative Study" (2024)
- - Compares CNN, RNN, and transformer approaches
- - Practical implementation with retail data
- - Key finding: Hybrid models outperform traditional clustering by 23%
-
-2. "Real-Time Customer Segmentation Using Ensemble Methods" (2023)
- - Focuses on scalability and production deployment
- - Combines Random Forest with gradient boosting
- - Includes case study: E-commerce company with 2M+ customers
-
-3. "Interpretable ML for Customer Segmentation" (2024)
- - Addresses the black-box problem
- - SHAP values for segment explanation
- - Critical for business stakeholder buy-in
-
-Would you like detailed summaries of any specific paper? I've noted this research in my research notes for future reference.
-
-Analysis:
-- Email agent updated interaction_history (logged meeting request)
-- Research agent read interaction_history (saw email context and ML project)
-- Research agent understands broader context without explicit briefing
-- Research agent tailored search to detailed explanations (user_preferences)
-- Demonstrates cross-agent context sharing through shared blocks
-```
-
-
-### Scenario 4: Financial Privacy Boundary
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 4: Testing Financial Data Privacy Boundary")
-print("=" * 70)
-print("\nUser → Research Agent: 'What's my budget for attending the ML conference?'\n")
-
-response = client.agents.messages.create(
- agent_id=research_agent.id,
- messages=[{
- "role": "user",
- "content": "I'm thinking of attending a machine learning conference. What's my current budget situation? Can I afford a $1,200 conference ticket?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Research Agent: {msg.content}\n")
-
-print("Analysis:")
-print("- Research agent does NOT have access to financial_data")
-print("- Research agent cannot answer budget questions")
-print("- Research agent should suggest consulting Finance agent or Coordinator")
-print("- Demonstrates privacy boundaries working correctly")
-```
-
-
-```
-======================================================================
-SCENARIO 4: Testing Financial Data Privacy Boundary
-======================================================================
-
-User → Research Agent: 'What's my budget for attending the ML conference?'
-
-Research Agent: I don't have access to your financial data or budget information, so I can't advise on whether you can afford the $1,200 conference ticket.
-
-For budget questions, you should consult:
-- Finance Agent: Specializes in budget tracking and expense management
-- Coordinator Agent: Has visibility into all your information including finances
-
-I can help you research which ML conferences would be most valuable for your customer segmentation project, but financial decisions should go through the appropriate agents with financial access.
-
-Would you like me to research top ML conferences for customer segmentation instead?
-
-Analysis:
-- Research agent does NOT have access to financial_data
-- Research agent cannot answer budget questions
-- Research agent should suggest consulting Finance agent or Coordinator
-- Demonstrates privacy boundaries working correctly
-```
-
-
-
-Financial data privacy is critical. Notice how the Research agent **gracefully handles** its lack of access by redirecting to appropriate agents, rather than hallucinating budget information.
-
-
-### Scenario 5: Finance Agent Updates Budget
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 5: Finance Agent with Financial Access")
-print("=" * 70)
-print("\nUser → Finance Agent: 'Can I afford a $1,200 ML conference ticket? Check my budget.'\n")
-
-response = client.agents.messages.create(
- agent_id=finance_agent.id,
- messages=[{
- "role": "user",
- "content": "I want to attend a machine learning conference that costs $1,200. Can I afford this given my current budget? What's my financial situation?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type
diff --git a/fern/pages/tutorials/draft/shared-memory-4-enterprise.mdx b/fern/pages/tutorials/draft/shared-memory-4-enterprise.mdx
deleted file mode 100644
index f009a891..00000000
--- a/fern/pages/tutorials/draft/shared-memory-4-enterprise.mdx
+++ /dev/null
@@ -1,1975 +0,0 @@
----
-title: "Shared Memory Part 4: Enterprise Multi-Team System"
-subtitle: Build a complete enterprise system with departments and hierarchies
-slug: cookbooks/shared-memory-enterprise
----
-
-This tutorial demonstrates how to build a complete enterprise multi-agent system with multiple departments, hierarchical block architecture, and cross-team coordination.
-
-## What You'll Learn
-
-- Creating hierarchical block architectures (company → department → team)
-- Implementing department-level isolation
-- Building cross-department coordination mechanisms
-- Managing executive-level aggregation and oversight
-- Scaling shared memory to 10+ agents with complex access patterns
-
-## Prerequisites
-
-```bash
-pip install letta-client
-```
-
-Set your Letta API key:
-```bash
-export LETTA_API_KEY="your-api-key-here"
-```
-
-## Architecture Overview
-
-This tutorial creates a complete enterprise system with:
-- **3 Departments**: Sales, Engineering, HR
-- **10 Agents**: 1 CEO, 3 Directors, 6 Workers
-- **12 Memory Blocks**: Company-wide, department-specific, cross-department, executive, and private blocks
-
-```
-TechCorp AI Solutions
-├── CEO (Executive Dashboard)
-├── Sales Department
-│ ├── Sales Director
-│ ├── Sales Rep 1
-│ └── Sales Rep 2
-├── Engineering Department
-│ ├── Engineering Director
-│ ├── Engineer 1
-│ └── Engineer 2
-└── HR Department
- ├── HR Director
- └── HR Rep 1
-```
-
-## Part 1: Company-Wide Blocks
-
-```python
-"""
-Tutorial 4: Enterprise Multi-Team System
-========================================
-
-Build a complete enterprise with departments and hierarchies.
-"""
-
-from letta import Letta
-
-# Initialize client
-client = Letta()
-
-print("=" * 70)
-print("TUTORIAL 4: Enterprise Multi-Team System")
-print("=" * 70)
-print()
-
-# ============================================================================
-# STEP 1: Create Company-Wide Blocks (Read-Only, All Agents)
-# ============================================================================
-print("STEP 1: Creating company-wide blocks (all agents, read-only)...\n")
-
-company_mission = client.blocks.create(
- label="company_mission",
- description="Company mission, values, and vision. Read-only for all agents.",
- value="""
-=== TECHCORP AI SOLUTIONS ===
-Company Mission & Values
-
-MISSION:
-Building ethical AI systems that empower businesses and respect user privacy.
-
-CORE VALUES:
-1. Transparency - Clear about AI capabilities and limitations
-2. Privacy - User data protection is paramount
-3. Accuracy - Rigorous testing and validation
-4. Collaboration - Cross-functional teamwork
-5. Innovation - Pushing boundaries responsibly
-
-VISION 2025:
-Become the most trusted AI solutions provider for enterprise customers.
-
-COMPANY GUIDELINES:
-- Always prioritize ethical considerations
-- Maintain open communication across departments
-- Customer success is our success
-- Continuous learning and improvement
-""",
- limit=4000
-)
-
-print(f"✓ Created company_mission: {company_mission.id}")
-print(f" Access: ALL 10 agents (read-only)")
-print()
-
-company_policies = client.blocks.create(
- label="company_policies",
- description="Company-wide policies and procedures. Read-only for all agents.",
- value="""
-=== COMPANY POLICIES ===
-
-COMMUNICATION POLICY:
-- Respond to customer inquiries within 24 hours
-- Internal team communication: Slack preferred
-- Cross-department coordination: Use shared project blocks
-
-DATA SECURITY POLICY:
-- Never share customer data across departments without consent
-- HR data is strictly confidential (HR department only)
-- Financial data requires executive approval to share
-
-WORK HOURS:
-- Core hours: 10 AM - 4 PM (all time zones)
-- Flexible scheduling outside core hours
-- Respect work-life balance
-
-ESCALATION POLICY:
-- Team member → Department Director → CEO
-- Cross-department issues → Use cross_dept_projects block
-- Urgent matters → CEO direct escalation
-""",
- limit=5000
-)
-
-print(f"✓ Created company_policies: {company_policies.id}")
-print(f" Access: ALL 10 agents (read-only)")
-print()
-
-# ============================================================================
-# STEP 2: Create Department-Specific Blocks (Read/Write, Department Only)
-# ============================================================================
-print("STEP 2: Creating department-specific blocks...\n")
-
-sales_knowledge = client.blocks.create(
- label="sales_knowledge",
- description="Sales team knowledge base. Sales department only.",
- value="""
-=== SALES KNOWLEDGE BASE ===
-Last Updated: 2024-10-08
-
-PRODUCT PRICING:
-- Starter Plan: $99/month
-- Professional Plan: $299/month
-- Enterprise Plan: Custom pricing (contact sales)
-
-SALES PLAYBOOK:
-- Lead qualification: BANT framework (Budget, Authority, Need, Timeline)
-- Demo best practices: Focus on customer pain points
-- Closing techniques: Trial close, assumptive close
-
-CUSTOMER ACCOUNTS:
-- Total Active Customers: 247
-- Pipeline Value: $2.3M
-- Avg Deal Size: $15,000
-
-COMPETITIVE INTELLIGENCE:
-- Main competitors: CompetitorX, CompetitorY
-- Our advantages: Better privacy features, faster implementation
-- Price positioning: Premium but justified by ROI
-
-SALES TARGETS Q4:
-- Team Target: $1.5M in new revenue
-- Rep 1 Target: $600K
-- Rep 2 Target: $600K
-- Director Target: Strategic accounts $300K
-""",
- limit=10000
-)
-
-print(f"✓ Created sales_knowledge: {sales_knowledge.id}")
-print(f" Access: Sales Director, Sales Rep 1, Sales Rep 2 (read/write)")
-print()
-
-engineering_specs = client.blocks.create(
- label="engineering_specs",
- description="Engineering specifications and technical docs. Engineering department only.",
- value="""
-=== ENGINEERING SPECIFICATIONS ===
-Last Updated: 2024-10-08
-
-CURRENT SPRINT (Oct 8-22):
-- Story Points: 42
-- Completed: 18
-- In Progress: 12
-- Blocked: 0
-
-ACTIVE PROJECTS:
-1. API v3.0 Migration
- Status: 60% complete
- Owner: Engineer 1
- Priority: HIGH
-
-2. Performance Optimization
- Status: 30% complete
- Owner: Engineer 2
- Priority: MEDIUM
-
-3. Security Audit Remediation
- Status: In Planning
- Owner: Engineering Director
- Priority: HIGH
-
-TECHNICAL STACK:
-- Backend: Python, FastAPI
-- Frontend: React, TypeScript
-- Database: PostgreSQL, Redis
-- Infrastructure: AWS, Kubernetes
-
-ENGINEERING STANDARDS:
-- Code review required for all PRs
-- 80% test coverage minimum
-- Documentation required for new features
-- Weekly architecture reviews
-
-KNOWN ISSUES:
-- API latency on large datasets (Engineer 1 investigating)
-- Mobile app occasional crashes (Engineer 2 working on fix)
-""",
- limit=12000
-)
-
-print(f"✓ Created engineering_specs: {engineering_specs.id}")
-print(f" Access: Engineering Director, Engineer 1, Engineer 2 (read/write)")
-print()
-
-hr_employee_data = client.blocks.create(
- label="hr_employee_data",
- description="HR employee data and records. HR department only - CONFIDENTIAL.",
- value="""
-=== HR EMPLOYEE DATA ===
-Last Updated: 2024-10-08
-CONFIDENTIAL - HR DEPARTMENT ONLY
-
-EMPLOYEE HEADCOUNT:
-- Total Employees: 47
-- Sales: 12
-- Engineering: 18
-- HR: 4
-- Operations: 8
-- Executive: 5
-
-ACTIVE HR CASES:
-1. Employee ID #2847 - Performance improvement plan
- Status: Week 2 of 6
- Handler: HR Rep 1
-
-2. Employee ID #1923 - Parental leave request
- Status: Approved, starts Oct 15
- Handler: HR Director
-
-RECRUITING:
-- Open Positions: 5
- * Senior Engineer (2 openings)
- * Sales Rep (1 opening)
- * HR Coordinator (1 opening)
- * Product Manager (1 opening)
-- Interviews This Week: 8 scheduled
-
-BENEFITS & COMPENSATION:
-- Annual review cycle: November
-- Benefits enrollment: Open now through Oct 31
-- 401k match: 5%
-
-COMPANY CULTURE:
-- Employee satisfaction: 4.2/5 (recent survey)
-- Retention rate: 92%
-- Diversity initiatives: On track
-""",
- limit=10000
-)
-
-print(f"✓ Created hr_employee_data: {hr_employee_data.id}")
-print(f" Access: HR Director, HR Rep 1 ONLY (read/write)")
-print(f" ⚠️ CONFIDENTIAL - Other departments CANNOT access")
-print()
-
-# ============================================================================
-# STEP 3: Create Cross-Department Coordination Block
-# ============================================================================
-print("STEP 3: Creating cross-department coordination block...\n")
-
-cross_dept_projects = client.blocks.create(
- label="cross_dept_projects",
- description="Cross-department projects and coordination. All directors + CEO.",
- value="""
-=== CROSS-DEPARTMENT PROJECTS ===
-Last Updated: 2024-10-08
-
-ACTIVE CROSS-DEPT PROJECTS:
-----------------------------
-
-PROJECT: Enterprise Customer Onboarding Improvement
-Teams: Sales + Engineering
-Status: Planning Phase
-Priority: HIGH
-
-Background:
-- Sales is closing large enterprise deals
-- Engineering needs to improve onboarding experience
-- Customer feedback: Onboarding takes too long
-
-Action Items:
-- [ ] Sales: Provide customer feedback summary (Due: Oct 10)
-- [ ] Engineering: Audit current onboarding flow (Due: Oct 12)
-- [ ] Joint meeting scheduled: Oct 15
-
-PROJECT: Recruiting Pipeline for Engineering
-Teams: HR + Engineering
-Status: Active
-Priority: MEDIUM
-
-Background:
-- 2 senior engineer positions open
-- Need technical interview process improvement
-
-Action Items:
-- [x] HR: Post job descriptions (Completed)
-- [ ] Engineering: Review candidates (Ongoing)
-- [ ] HR + Engineering: Interview coordination
-
-COMPLETED PROJECTS:
-----------------------------
-- Q3 Sales Enablement Training (Sales + Engineering) - Sep 2024
-""",
- limit=8000
-)
-
-print(f"✓ Created cross_dept_projects: {cross_dept_projects.id}")
-print(f" Access: CEO, Sales Director, Engineering Director, HR Director (read/write)")
-print(f" Purpose: Coordinate work across departments")
-print()
-
-# ============================================================================
-# STEP 4: Create Executive Dashboard Block (CEO Only)
-# ============================================================================
-print("STEP 4: Creating executive dashboard block (CEO only)...\n")
-
-executive_dashboard = client.blocks.create(
- label="executive_dashboard",
- description="Executive dashboard with company-wide metrics. CEO only.",
- value="""
-=== EXECUTIVE DASHBOARD ===
-Last Updated: 2024-10-08
-CEO EYES ONLY
-
-COMPANY HEALTH METRICS:
-------------------------
-Revenue (Q4 to date): $3.2M / $4.5M target (71%)
-Customer Satisfaction: 4.3/5
-Employee Satisfaction: 4.2/5
-Churn Rate: 3% (target: <5%)
-
-DEPARTMENT PERFORMANCE:
-------------------------
-Sales:
-- Pipeline: $2.3M (healthy)
-- Q4 Target: $1.5M new revenue
-- On track: Yes
-
-Engineering:
-- Sprint velocity: Steady
-- Critical projects: 2 on track, 1 needs attention
-- Technical debt: Manageable
-
-HR:
-- Headcount: 47 (target: 50 by EOY)
-- Open positions: 5
-- Retention: 92% (excellent)
-
-STRATEGIC PRIORITIES:
-------------------------
-1. Close 3 enterprise deals by EOQ (Sales + Engineering)
-2. Complete API v3.0 migration (Engineering)
-3. Hire 2 senior engineers (HR + Engineering)
-4. Maintain >90% employee retention (HR)
-
-RISK FACTORS:
-------------------------
-- Competition increasing in enterprise segment
-- Engineering team at capacity
-- Need to scale customer support
-""",
- limit=8000
-)
-
-print(f"✓ Created executive_dashboard: {executive_dashboard.id}")
-print(f" Access: CEO ONLY (read/write)")
-print(f" ⚠️ Executive-level metrics not visible to departments")
-print()
-
-print("=" * 70)
-print("BLOCK ARCHITECTURE CREATED")
-print("=" * 70)
-print("""
-BLOCK HIERARCHY:
-================
-
-TIER 1: Company-Wide (Read-Only, All 10 agents)
- - company_mission
- - company_policies
-
-TIER 2: Department-Specific (Read/Write, Department only)
- - sales_knowledge (Sales dept: 3 agents)
- - engineering_specs (Engineering dept: 3 agents)
- - hr_employee_data (HR dept: 2 agents)
-
-TIER 3: Cross-Department (Read/Write, Directors + CEO)
- - cross_dept_projects (4 agents: CEO + 3 Directors)
-
-TIER 4: Executive (Read/Write, CEO only)
- - executive_dashboard (1 agent: CEO)
-
-Total Shared Blocks: 7
-Private Blocks per Agent: 1 each (10 total)
-""")
-```
-
-
-```
-======================================================================
-TUTORIAL 4: Enterprise Multi-Team System
-======================================================================
-
-STEP 1: Creating company-wide blocks (all agents, read-only)...
-
-✓ Created company_mission: block-p1q2r3s4-t5u6-7890-pqrs-tu1234567890
- Access: ALL 10 agents (read-only)
-
-✓ Created company_policies: block-q2r3s4t5-u6v7-8901-qrst-uv2345678901
- Access: ALL 10 agents (read-only)
-
-STEP 2: Creating department-specific blocks...
-
-✓ Created sales_knowledge: block-r3s4t5u6-v7w8-9012-rstu-vw3456789012
- Access: Sales Director, Sales Rep 1, Sales Rep 2 (read/write)
-
-✓ Created engineering_specs: block-s4t5u6v7-w8x9-0123-stuv-wx4567890123
- Access: Engineering Director, Engineer 1, Engineer 2 (read/write)
-
-✓ Created hr_employee_data: block-t5u6v7w8-x9y0-1234-tuvw-xy5678901234
- Access: HR Director, HR Rep 1 ONLY (read/write)
- ⚠️ CONFIDENTIAL - Other departments CANNOT access
-
-STEP 3: Creating cross-department coordination block...
-
-✓ Created cross_dept_projects: block-u6v7w8x9-y0z1-2345-uvwx-yz6789012345
- Access: CEO, Sales Director, Engineering Director, HR Director (read/write)
- Purpose: Coordinate work across departments
-
-STEP 4: Creating executive dashboard block (CEO only)...
-
-✓ Created executive_dashboard: block-v7w8x9y0-z1a2-3456-vwxy-za7890123456
- Access: CEO ONLY (read/write)
- ⚠️ Executive-level metrics not visible to departments
-
-======================================================================
-BLOCK ARCHITECTURE CREATED
-======================================================================
-...
-```
-
-
-## Part 2: Agent Creation
-
-### Executive Layer
-
-```python
-# ============================================================================
-# STEP 5: Create CEO Agent
-# ============================================================================
-print("\nSTEP 5: Creating CEO Agent (access to all cross-dept and executive blocks)...\n")
-
-ceo_notes = client.blocks.create(
- label="ceo_executive_notes",
- description="Private CEO notes and strategic planning.",
- value="""
-=== CEO EXECUTIVE NOTES ===
-
-STRATEGIC FOCUS Q4:
-- Enterprise market expansion
-- Team scaling (controlled growth)
-- Product-market fit validation
-
-KEY RELATIONSHIPS:
-- Board meeting: Monthly
-- Investor updates: Quarterly
-- All-hands: Bi-weekly
-
-DECISION LOG:
-(None yet)
-""",
- limit=5000
-)
-
-ceo = client.agents.create(
- name="CEO",
- model="openai/gpt-4o",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am the CEO of TechCorp AI Solutions.
-
-My role:
-- Set company vision and strategy
-- Monitor overall company health
-- Coordinate cross-department initiatives
-- Make executive decisions
-- Support department directors
-
-My access:
-- Company-wide blocks: company_mission, company_policies (read-only)
-- Cross-department: cross_dept_projects (read/write)
-- Executive: executive_dashboard (read/write, private)
-- CEO notes (private)
-
-Note: I do NOT have direct access to department-specific blocks (sales_knowledge,
-engineering_specs, hr_employee_data). I rely on directors to surface relevant
-information through cross_dept_projects.
-
-My style:
-- Strategic and big-picture focused
-- Empowering to directors
-- Data-driven decision making
-- Transparent communication
-"""
- },
- {
- "label": "human",
- "value": "Name: Board/Stakeholder\nRole: Company oversight"
- }
- ],
- block_ids=[company_mission.id, company_policies.id, cross_dept_projects.id, executive_dashboard.id, ceo_notes.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created CEO: {ceo.id}")
-print(f" Shared blocks: company_mission, company_policies, cross_dept_projects, executive_dashboard")
-print(f" Private blocks: ceo_executive_notes")
-print(f" Note: NO direct access to department blocks")
-print()
-
-### Sales Department
-
-print("STEP 6: Creating Sales Department agents...\n")
-
-# Sales Director
-sales_director_notes = client.blocks.create(
- label="sales_director_notes",
- description="Private sales director notes and strategy.",
- value="""
-=== SALES DIRECTOR NOTES ===
-
-STRATEGIC ACCOUNTS:
-- 3 enterprise deals in pipeline
-- Total value: $850K
-
-TEAM MANAGEMENT:
-- Rep 1: Strong performer, on track
-- Rep 2: Ramping up, needs coaching
-
-PRIORITIES:
-- Close Acme Corp deal ($300K)
-- Coordinate with Engineering on onboarding
-""",
- limit=5000
-)
-
-sales_director = client.agents.create(
- name="Sales_Director",
- model="openai/gpt-4o",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am the Sales Director at TechCorp AI Solutions.
-
-My role:
-- Lead sales team and strategy
-- Manage strategic accounts
-- Coordinate with other departments (Engineering, HR)
-- Report to CEO on sales performance
-
-My access:
-- Company-wide: company_mission, company_policies (read-only)
-- Department: sales_knowledge (read/write, shared with team)
-- Cross-dept: cross_dept_projects (read/write, coordinate with other directors)
-- Private: sales_director_notes
-
-My style:
-- Results-oriented and strategic
-- Collaborative with Engineering
-- Supportive of sales reps
-- Customer-focused
-"""
- },
- {
- "label": "human",
- "value": "Name: CEO or Sales Team\nRole: Sales leadership"
- }
- ],
- block_ids=[company_mission.id, company_policies.id, sales_knowledge.id, cross_dept_projects.id, sales_director_notes.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created Sales Director: {sales_director.id}")
-print()
-
-# Sales Rep 1
-sales_rep1_notes = client.blocks.create(
- label="sales_rep1_notes",
- description="Private sales rep 1 notes and leads.",
- value="=== SALES REP 1 NOTES ===\n\nActive Leads: 12\nQ4 Target: $600K\nClosed This Quarter: $420K\n",
- limit=4000
-)
-
-sales_rep1 = client.agents.create(
- name="Sales_Rep_1",
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am Sales Rep 1 at TechCorp AI Solutions.
-
-My role:
-- Manage sales pipeline
-- Close deals
-- Coordinate with Sales Director
-- Reference sales playbook and pricing
-
-My access:
-- Company-wide: company_mission, company_policies (read-only)
-- Department: sales_knowledge (read/write, shared with sales team)
-- Private: sales_rep1_notes
-
-My style:
-- Customer-focused and consultative
-- Data-driven
-- Team player
-"""
- },
- {
- "label": "human",
- "value": "Name: Sales Director or Customer\nRole: Sales execution"
- }
- ],
- block_ids=[company_mission.id, company_policies.id, sales_knowledge.id, sales_rep1_notes.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created Sales Rep 1: {sales_rep1.id}")
-print()
-
-# Sales Rep 2
-sales_rep2_notes = client.blocks.create(
- label="sales_rep2_notes",
- description="Private sales rep 2 notes and leads.",
- value="=== SALES REP 2 NOTES ===\n\nActive Leads: 15\nQ4 Target: $600K\nClosed This Quarter: $380K\n",
- limit=4000
-)
-
-sales_rep2 = client.agents.create(
- name="Sales_Rep_2",
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am Sales Rep 2 at TechCorp AI Solutions.
-
-My role:
-- Manage sales pipeline
-- Close deals
-- Coordinate with Sales Director
-- Reference sales playbook and pricing
-
-My access:
-- Company-wide: company_mission, company_policies (read-only)
-- Department: sales_knowledge (read/write, shared with sales team)
-- Private: sales_rep2_notes
-
-My style:
-- Relationship-focused
-- Strategic account management
-- Collaborative
-"""
- },
- {
- "label": "human",
- "value": "Name: Sales Director or Customer\nRole: Sales execution"
- }
- ],
- block_ids=[company_mission.id, company_policies.id, sales_knowledge.id, sales_rep2_notes.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created Sales Rep 2: {sales_rep2.id}")
-print()
-
-### Engineering Department
-
-print("STEP 7: Creating Engineering Department agents...\n")
-
-# Engineering Director
-eng_director_notes = client.blocks.create(
- label="eng_director_notes",
- description="Private engineering director notes and planning.",
- value="""
-=== ENGINEERING DIRECTOR NOTES ===
-
-TECHNICAL STRATEGY:
-- API v3.0 migration: Top priority
-- Security audit: Must complete Q4
-- Performance optimization: Ongoing
-
-TEAM CAPACITY:
-- Need 2 senior engineers
-- Current team at 85% capacity
-- Considering contractors for peak load
-
-CROSS-DEPT COORDINATION:
-- Sales asking for faster onboarding
-- HR coordinating on hiring
-""",
- limit=5000
-)
-
-eng_director = client.agents.create(
- name="Engineering_Director",
- model="openai/gpt-4o",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am the Engineering Director at TechCorp AI Solutions.
-
-My role:
-- Lead engineering team and technical strategy
-- Manage engineering projects and sprints
-- Coordinate with Sales and HR departments
-- Report to CEO on technical progress
-
-My access:
-- Company-wide: company_mission, company_policies (read-only)
-- Department: engineering_specs (read/write, shared with engineers)
-- Cross-dept: cross_dept_projects (read/write, coordinate with other directors)
-- Private: eng_director_notes
-
-My style:
-- Technical and strategic
-- Quality-focused
-- Collaborative with Sales (understand customer needs)
-- Supportive of engineering team
-"""
- },
- {
- "label": "human",
- "value": "Name: CEO or Engineering Team\nRole: Engineering leadership"
- }
- ],
- block_ids=[company_mission.id, company_policies.id, engineering_specs.id, cross_dept_projects.id, eng_director_notes.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created Engineering Director: {eng_director.id}")
-print()
-
-# Engineer 1
-engineer1_notes = client.blocks.create(
- label="engineer1_notes",
- description="Private engineer 1 notes and tasks.",
- value="=== ENGINEER 1 NOTES ===\n\nCurrent Project: API v3.0 Migration (60% complete)\nBlockers: None\nNext: Database schema updates\n",
- limit=4000
-)
-
-engineer1 = client.agents.create(
- name="Engineer_1",
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am Engineer 1 at TechCorp AI Solutions.
-
-My role:
-- Develop and maintain software
-- Work on assigned projects (currently: API v3.0 migration)
-- Follow engineering standards
-- Coordinate with Engineering Director
-
-My access:
-- Company-wide: company_mission, company_policies (read-only)
-- Department: engineering_specs (read/write, shared with engineering team)
-- Private: engineer1_notes
-
-My style:
-- Detail-oriented and thorough
-- Quality-focused (testing, documentation)
-- Collaborative with team
-"""
- },
- {
- "label": "human",
- "value": "Name: Engineering Director or Team\nRole: Software development"
- }
- ],
- block_ids=[company_mission.id, company_policies.id, engineering_specs.id, engineer1_notes.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created Engineer 1: {engineer1.id}")
-print()
-
-# Engineer 2
-engineer2_notes = client.blocks.create(
- label="engineer2_notes",
- description="Private engineer 2 notes and tasks.",
- value="=== ENGINEER 2 NOTES ===\n\nCurrent Project: Performance Optimization (30% complete)\nBlockers: None\nNext: Query optimization for large datasets\n",
- limit=4000
-)
-
-engineer2 = client.agents.create(
- name="Engineer_2",
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am Engineer 2 at TechCorp AI Solutions.
-
-My role:
-- Develop and maintain software
-- Work on assigned projects (currently: Performance optimization)
-- Follow engineering standards
-- Coordinate with Engineering Director
-
-My access:
-- Company-wide: company_mission, company_policies (read-only)
-- Department: engineering_specs (read/write, shared with engineering team)
-- Private: engineer2_notes
-
-My style:
-- Performance-focused
-- Data-driven optimization
-- Pragmatic problem-solving
-"""
- },
- {
- "label": "human",
- "value": "Name: Engineering Director or Team\nRole: Software development"
- }
- ],
- block_ids=[company_mission.id, company_policies.id, engineering_specs.id, engineer2_notes.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created Engineer 2: {engineer2.id}")
-print()
-
-### HR Department
-
-print("STEP 8: Creating HR Department agents...\n")
-
-# HR Director
-hr_director_notes = client.blocks.create(
- label="hr_director_notes",
- description="Private HR director notes and planning.",
- value="""
-=== HR DIRECTOR NOTES ===
-
-RECRUITING PRIORITIES:
-- 2 Senior Engineers (urgent for Engineering)
-- 1 Sales Rep (Sales scaling)
-- 1 HR Coordinator (team growth)
-
-EMPLOYEE RELATIONS:
-- 1 performance improvement plan (ongoing)
-- Annual review prep (November)
-- Benefits enrollment (ends Oct 31)
-
-CROSS-DEPT COORDINATION:
-- Engineering: Hiring coordination
-- Sales: New rep onboarding planning
-""",
- limit=5000
-)
-
-hr_director = client.agents.create(
- name="HR_Director",
- model="openai/gpt-4o",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am the HR Director at TechCorp AI Solutions.
-
-My role:
-- Lead HR strategy and operations
-- Manage recruiting, employee relations, benefits
-- Coordinate with other departments on hiring
-- Maintain confidential employee data
-- Report to CEO on HR metrics
-
-My access:
-- Company-wide: company_mission, company_policies (read-only)
-- Department: hr_employee_data (read/write, CONFIDENTIAL, HR only)
-- Cross-dept: cross_dept_projects (read/write, coordinate with other directors)
-- Private: hr_director_notes
-
-My style:
-- People-focused and empathetic
-- Confidentiality is paramount
-- Strategic about talent and culture
-- Collaborative with all departments
-"""
- },
- {
- "label": "human",
- "value": "Name: CEO or HR Team\nRole: HR leadership"
- }
- ],
- block_ids=[company_mission.id, company_policies.id, hr_employee_data.id, cross_dept_projects.id, hr_director_notes.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created HR Director: {hr_director.id}")
-print()
-
-# HR Rep 1
-hr_rep1_notes = client.blocks.create(
- label="hr_rep1_notes",
- description="Private HR rep 1 notes and cases.",
- value="=== HR REP 1 NOTES ===\n\nActive Cases: 2\n- Performance improvement plan (Employee #2847)\n- Benefits questions (3 employees)\n",
- limit=4000
-)
-
-hr_rep1 = client.agents.create(
- name="HR_Rep_1",
- model="openai/gpt-4o-mini",
- embedding="openai/text-embedding-3-small",
- memory_blocks=[
- {
- "label": "persona",
- "value": """I am HR Rep 1 at TechCorp AI Solutions.
-
-My role:
-- Handle employee relations cases
-- Support recruiting efforts
-- Manage benefits administration
-- Maintain confidentiality
-- Coordinate with HR Director
-
-My access:
-- Company-wide: company_mission, company_policies (read-only)
-- Department: hr_employee_data (read/write, CONFIDENTIAL, HR only)
-- Private: hr_rep1_notes
-
-My style:
-- Empathetic and supportive
-- Confidentiality is critical
-- Detail-oriented with cases
-- Helpful and responsive
-"""
- },
- {
- "label": "human",
- "value": "Name: HR Director or Employees\nRole: HR operations"
- }
- ],
- block_ids=[company_mission.id, company_policies.id, hr_employee_data.id, hr_rep1_notes.id],
- tools=["core_memory_append", "core_memory_replace"],
-)
-
-print(f"✓ Created HR Rep 1: {hr_rep1.id}")
-print()
-
-print("=" * 70)
-print("ENTERPRISE ORGANIZATION CREATED - 10 AGENTS")
-print("=" * 70)
-print("""
-ORGANIZATION STRUCTURE:
-=======================
-
-CEO (1 agent)
- └── Access: company_mission, company_policies, cross_dept_projects, executive_dashboard
-
-Sales Department (3 agents)
- ├── Sales Director
- │ └── Access: company blocks + sales_knowledge + cross_dept_projects
- ├── Sales Rep 1
- │ └── Access: company blocks + sales_knowledge
- └── Sales Rep 2
- └── Access: company blocks + sales_knowledge
-
-Engineering Department (3 agents)
- ├── Engineering Director
- │ └── Access: company blocks + engineering_specs + cross_dept_projects
- ├── Engineer 1
- │ └── Access: company blocks + engineering_specs
- └── Engineer 2
- └── Access: company blocks + engineering_specs
-
-HR Department (2 agents)
- ├── HR Director
- │ └── Access: company blocks + hr_employee_data + cross_dept_projects
- └── HR Rep 1
- └── Access: company blocks + hr_employee_data
-
-BLOCK ACCESS SUMMARY:
-=====================
-- 10 agents total
-- 7 shared blocks
-- 10 private blocks (1 per agent)
-- Department isolation enforced
-- Cross-department coordination via cross_dept_projects
-- Executive oversight via executive_dashboard
-""")```
-
-## Part 3: Message Flow Scenarios
-
-### Scenario 1: Company-Wide Policy Check (Vertical Consistency)
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 1: Company-Wide Policy Check Across All Departments")
-print("=" * 70)
-
-# Test that all departments see the same company mission
-print("\nTesting that Sales Rep, Engineer, and HR Rep all see same company mission...\n")
-
-print("User → Sales Rep 1: 'What is our company mission?'\n")
-response_sales = client.agents.messages.create(
- agent_id=sales_rep1.id,
- messages=[{
- "role": "user",
- "content": "What is our company's mission?"
- }]
-)
-
-for msg in response_sales.messages:
- if msg.message_type == "assistant_message":
- print(f"Sales Rep 1: {msg.content}\n")
-
-print("User → Engineer 1: 'What is our company mission?'\n")
-response_eng = client.agents.messages.create(
- agent_id=engineer1.id,
- messages=[{
- "role": "user",
- "content": "What is our company's mission?"
- }]
-)
-
-for msg in response_eng.messages:
- if msg.message_type == "assistant_message":
- print(f"Engineer 1: {msg.content}\n")
-
-print("User → HR Rep 1: 'What is our company mission?'\n")
-response_hr = client.agents.messages.create(
- agent_id=hr_rep1.id,
- messages=[{
- "role": "user",
- "content": "What is our company's mission?"
- }]
-)
-
-for msg in response_hr.messages:
- if msg.message_type == "assistant_message":
- print(f"HR Rep 1: {msg.content}\n")
-
-print("Analysis:")
-print("- All three agents from different departments accessed company_mission")
-print("- All provided identical mission statement")
-print("- Demonstrates vertical consistency across entire organization")
-print("- Company-wide blocks ensure unified messaging")
-```
-
-
-```
-======================================================================
-SCENARIO 1: Company-Wide Policy Check Across All Departments
-======================================================================
-
-Testing that Sales Rep, Engineer, and HR Rep all see same company mission...
-
-User → Sales Rep 1: 'What is our company mission?'
-
-Sales Rep 1: Our company mission at TechCorp AI Solutions is "Building ethical AI
-systems that empower businesses and respect user privacy." We focus on transparency,
-privacy, accuracy, collaboration, and innovation as our core values.
-
-User → Engineer 1: 'What is our company mission?'
-
-Engineer 1: TechCorp's mission is to build ethical AI systems that empower businesses
-while respecting user privacy. Our core values are transparency, privacy, accuracy,
-collaboration, and innovation.
-
-User → HR Rep 1: 'What is our company mission?'
-
-HR Rep 1: Our mission is building ethical AI systems that empower businesses and
-respect user privacy. We prioritize transparency, privacy, accuracy, collaboration,
-and innovation in everything we do.
-
-Analysis:
-- All three agents from different departments accessed company_mission
-- All provided identical mission statement
-- Demonstrates vertical consistency across entire organization
-- Company-wide blocks ensure unified messaging
-```
-
-
-### Scenario 2: Department-Isolated Knowledge
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 2: Department Isolation - Sales Cannot Access Engineering Data")
-print("=" * 70)
-print("\nUser → Sales Rep 1: 'What engineering projects are currently in progress?'\n")
-
-response = client.agents.messages.create(
- agent_id=sales_rep1.id,
- messages=[{
- "role": "user",
- "content": "I need to know what engineering projects are currently in progress. What's the status of the API v3.0 migration?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Sales Rep 1: {msg.content}\n")
-
-print("Analysis:")
-print("- Sales Rep 1 does NOT have access to engineering_specs")
-print("- Cannot answer specific engineering project questions")
-print("- Should suggest asking Engineering Director or using cross_dept_projects")
-print("- Demonstrates department isolation working correctly")
-```
-
-
-```
-======================================================================
-SCENARIO 2: Department Isolation - Sales Cannot Access Engineering Data
-======================================================================
-
-User → Sales Rep 1: 'What engineering projects are currently in progress?'
-
-Sales Rep 1: I don't have direct access to Engineering's project details or the
-engineering_specs block. For specific information about the API v3.0 migration status,
-you should:
-
-1. Ask the Engineering Director directly
-2. Check the cross_dept_projects block if there's a related cross-department initiative
-3. Our Sales Director can coordinate with Engineering leadership
-
-I can tell you about our sales pipeline and customer needs, which might help inform
-engineering priorities if needed. Would that be helpful?
-
-Analysis:
-- Sales Rep 1 does NOT have access to engineering_specs
-- Cannot answer specific engineering project questions
-- Should suggest asking Engineering Director or using cross_dept_projects
-- Demonstrates department isolation working correctly
-```
-
-
-
-Department isolation is critical for security and information compartmentalization. Agents correctly recognize their access boundaries and direct users to appropriate channels.
-
-
-### Scenario 3: Cross-Department Coordination (Directors)
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 3: Sales Director Coordinates with Engineering via Cross-Dept Block")
-print("=" * 70)
-print("\nUser → Sales Director: 'Update the cross-department projects: Sales is hearing from customers that onboarding takes too long. We need Engineering's help to streamline it.'\n")
-
-response = client.agents.messages.create(
- agent_id=sales_director.id,
- messages=[{
- "role": "user",
- "content": "We're getting feedback from customers that the onboarding process takes too long. This is affecting deal closures. Can you update the cross-department projects block to coordinate with Engineering on improving the onboarding experience?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Sales Director: {msg.content}\n")
-
-# Check updated cross_dept_projects
-updated_cross_dept = client.blocks.retrieve(block_id=cross_dept_projects.id)
-print("Updated cross_dept_projects block:")
-print("-" * 70)
-print(updated_cross_dept.value[:800] + "...")
-print("-" * 70)
-
-print("\nAnalysis:")
-print("- Sales Director has access to cross_dept_projects")
-print("- Updated block with customer feedback and action items")
-print("- Engineering Director will see this update")
-print("- Demonstrates director-level cross-department coordination")
-```
-
-
-```
-======================================================================
-SCENARIO 3: Sales Director Coordinates with Engineering via Cross-Dept Block
-======================================================================
-
-User → Sales Director: 'Update the cross-department projects...'
-
-Sales Director: I've updated the cross_dept_projects block with this critical
-customer feedback. Here's what I've documented:
-
-**Added to Enterprise Customer Onboarding Improvement Project:**
-- Priority escalated to HIGH (affecting deal closures)
-- Customer feedback: Onboarding takes too long
-- Impact: Customers hesitating on large deals
-- Sales action item: Compile specific customer feedback with timeline expectations
-
-The Engineering Director will see this update and can respond with their technical
-assessment. This is exactly the kind of cross-functional coordination we need to
-maintain our competitive edge.
-
-Updated cross_dept_projects block:
-----------------------------------------------------------------------
-=== CROSS-DEPARTMENT PROJECTS ===
-Last Updated: 2024-10-08 14:45 PM
-
-ACTIVE CROSS-DEPT PROJECTS:
-----------------------------
-
-PROJECT: Enterprise Customer Onboarding Improvement
-Teams: Sales + Engineering
-Status: Planning Phase → ESCALATED
-Priority: HIGH → CRITICAL
-
-Background:
-- Sales is closing large enterprise deals
-- Engineering needs to improve onboarding experience
-- Customer feedback: Onboarding takes too long (affecting deal closures)
-- Sales reporting: Multiple customers citing onboarding as concern
-
-Action Items:
-- [x] Sales: Provide customer feedback summary (Sales Director - Completed)
-- [ ] Engineering: Audit current onboarding flow (Due: Oct 12)
-...
-----------------------------------------------------------------------
-
-Analysis:
-- Sales Director has access to cross_dept_projects
-- Updated block with customer feedback and action items
-- Engineering Director will see this update
-- Demonstrates director-level cross-department coordination
-```
-
-
-### Scenario 4: Engineering Director Sees Sales Update
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 4: Engineering Director Responds to Sales Request")
-print("=" * 70)
-print("\nUser → Engineering Director: 'Check the cross-department projects block. Any updates from other teams?'\n")
-
-response = client.agents.messages.create(
- agent_id=eng_director.id,
- messages=[{
- "role": "user",
- "content": "Check the cross-department projects block for any updates from other teams that need our attention."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Engineering Director: {msg.content}\n")
-
-print("Analysis:")
-print("- Engineering Director read cross_dept_projects")
-print("- Saw Sales Director's escalation")
-print("- Can now respond and coordinate internally")
-print("- Demonstrates asynchronous cross-department communication via shared blocks")
-```
-
-
-```
-======================================================================
-SCENARIO 4: Engineering Director Responds to Sales Request
-======================================================================
-
-User → Engineering Director: 'Check the cross-department projects block...'
-
-Engineering Director: I see an important escalation from the Sales Director:
-
-**Enterprise Customer Onboarding Improvement - Now CRITICAL Priority**
-
-The Sales team is reporting that onboarding delays are affecting deal closures. This
-is serious and needs immediate attention.
-
-Here's my response:
-
-1. **Immediate Action**: I'll have Engineer 1 conduct an onboarding flow audit this week
-2. **Technical Assessment**: We'll identify bottlenecks (likely account provisioning and initial setup)
-3. **Timeline**: Preliminary findings by Oct 12, implementation plan by Oct 15
-4. **Resource Allocation**: This takes priority over non-critical performance optimization work
-
-I'll update the cross_dept_projects block with our technical assessment once Engineer 1
-completes the audit. Let me also update the engineering_specs block to reflect this
-new priority for my team.
-
-This is excellent cross-department coordination - exactly what we need to stay customer-focused.
-
-Analysis:
-- Engineering Director read cross_dept_projects
-- Saw Sales Director's escalation
-- Can now respond and coordinate internally
-- Demonstrates asynchronous cross-department communication via shared blocks
-```
-
-
-
-The `cross_dept_projects` block acts as an **asynchronous coordination channel** between directors, eliminating the need for explicit messaging or meetings for routine coordination.
-
-
-### Scenario 5: HR Data Confidentiality
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 5: Testing HR Data Confidentiality")
-print("=" * 70)
-print("\nUser → Sales Director: 'Who is currently on a performance improvement plan?'\n")
-
-response = client.agents.messages.create(
- agent_id=sales_director.id,
- messages=[{
- "role": "user",
- "content": "I heard someone in the company is on a performance improvement plan. Who is it and what's the situation?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Sales Director: {msg.content}\n")
-
-print("Analysis:")
-print("- Sales Director does NOT have access to hr_employee_data")
-print("- Cannot access confidential HR information")
-print("- Should redirect to HR Director")
-print("- Demonstrates HR confidentiality protection")
-```
-
-
-```
-======================================================================
-SCENARIO 5: Testing HR Data Confidentiality
-======================================================================
-
-User → Sales Director: 'Who is currently on a performance improvement plan?'
-
-Sales Director: I don't have access to confidential HR employee data, including
-information about performance improvement plans. That information is restricted to
-the HR department only, as per our company policies on data security and employee
-privacy.
-
-If you have a legitimate need to know about a specific employee situation (for example,
-if it affects your team), you should:
-1. Contact the HR Director directly
-2. They'll assess whether sharing that information is appropriate
-3. Any discussions will be handled confidentially
-
-Is there a specific business concern I can help you with regarding team performance?
-
-Analysis:
-- Sales Director does NOT have access to hr_employee_data
-- Cannot access confidential HR information
-- Should redirect to HR Director
-- Demonstrates HR confidentiality protection
-```
-
-
-### Scenario 6: CEO Strategic Overview
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 6: CEO Reviews Company Status")
-print("=" * 70)
-print("\nUser → CEO: 'Give me a company-wide status update. What's happening across all departments?'\n")
-
-response = client.agents.messages.create(
- agent_id=ceo.id,
- messages=[{
- "role": "user",
- "content": "I need a comprehensive company update. What's the status across Sales, Engineering, and HR? Any cross-department issues I should be aware of?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"CEO: {msg.content}\n")
-
-print("Analysis:")
-print("- CEO accessed executive_dashboard (company-wide metrics)")
-print("- CEO accessed cross_dept_projects (coordination status)")
-print("- CEO does NOT have direct access to department-specific blocks")
-print("- CEO provides high-level overview based on aggregated data")
-print("- Demonstrates executive oversight without micromanagement")
-```
-
-
-```
-======================================================================
-SCENARIO 6: CEO Reviews Company Status
-======================================================================
-
-User → CEO: 'Give me a company-wide status update...'
-
-CEO: Here's the company-wide status based on my executive dashboard and cross-department
-coordination:
-
-**OVERALL COMPANY HEALTH:**
-- Revenue: $3.2M / $4.5M Q4 target (71% - on track)
-- Customer Satisfaction: 4.3/5 (strong)
-- Employee Satisfaction: 4.2/5 (healthy)
-- Churn Rate: 3% (below 5% target ✓)
-
-**DEPARTMENT STATUS:**
-
-*Sales*
-✓ Pipeline: $2.3M (healthy)
-✓ On track for $1.5M Q4 target
-⚠️ Issue: Customer feedback on slow onboarding (being addressed)
-
-*Engineering*
-✓ Sprint velocity steady
-✓ API v3.0 migration: 60% complete
-⚠️ Team at capacity - coordinating with HR on hiring
-
-*HR*
-✓ Employee retention: 92% (excellent)
-⚠️ 5 open positions (2 senior engineers critical for Engineering)
-✓ Recruiting pipeline active
-
-**CROSS-DEPARTMENT COORDINATION:**
-Active: Onboarding improvement (Sales + Engineering) - escalated to CRITICAL priority
-Active: Engineering hiring (HR + Engineering) - ongoing
-
-**STRATEGIC ASSESSMENT:**
-We're generally healthy, but the onboarding issue needs immediate attention as it's
-affecting deal closures. The fact that Sales and Engineering are already coordinating
-through cross_dept_projects is exactly the collaboration we need.
-
-Recommendation: I'll monitor the onboarding improvement project closely. If they need
-resources or executive intervention, I'm ready to support.
-
-Analysis:
-- CEO accessed executive_dashboard (company-wide metrics)
-- CEO accessed cross_dept_projects (coordination status)
-- CEO does NOT have direct access to department-specific blocks
-- CEO provides high-level overview based on aggregated data
-- Demonstrates executive oversight without micromanagement
-```
-
-
-
-The CEO has **strategic oversight** without micromanaging. The executive_dashboard provides aggregated metrics, while cross_dept_projects shows coordination, but the CEO doesn't see day-to-day department operations.
-
-
-### Scenario 7: Hierarchical Reporting (Bottom-Up)
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 7: Sales Rep Reports to Director (Bottom-Up Communication)")
-print("=" * 70)
-print("\nUser → Sales Rep 1: 'Log that you closed the Acme Corp deal for $300K in the sales knowledge base'\n")
-
-response = client.agents.messages.create(
- agent_id=sales_rep1.id,
- messages=[{
- "role": "user",
- "content": "Great news! I just closed the Acme Corp deal for $300K. Update the sales knowledge base with this win and update my personal Q4 numbers."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Sales Rep 1: {msg.content}\n")
-
-# Check updated sales_knowledge
-updated_sales = client.blocks.retrieve(block_id=sales_knowledge.id)
-print("Updated sales_knowledge block (excerpt):")
-print("-" * 70)
-print(updated_sales.value[:600] + "...")
-print("-" * 70)
-
-print("\nNow Sales Director checks the shared sales_knowledge...")
-print("User → Sales Director: 'Any wins from the team today?'\n")
-
-response = client.agents.messages.create(
- agent_id=sales_director.id,
- messages=[{
- "role": "user",
- "content": "Check the sales knowledge base - any wins from the team today?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Sales Director: {msg.content}\n")
-
-print("Analysis:")
-print("- Sales Rep 1 updated shared sales_knowledge block")
-print("- Sales Director immediately sees the update")
-print("- Demonstrates bottom-up information flow within department")
-print("- Director has visibility into team performance without explicit reporting")
-```
-
-
-```
-======================================================================
-SCENARIO 7: Sales Rep Reports to Director (Bottom-Up Communication)
-======================================================================
-
-User → Sales Rep 1: 'Log that you closed the Acme Corp deal for $300K...'
-
-Sales Rep 1: Excellent! I've updated the sales knowledge base with the Acme Corp win:
-
-**Deal Details Logged:**
-- Customer: Acme Corp
-- Deal Size: $300K
-- Close Date: Oct 8, 2024
-- Plan: Enterprise (custom pricing)
-- Rep: Sales Rep 1
-
-**Updated Q4 Numbers:**
-- My Q4 closed: $420K → $720K
-- Target: $600K
-- Achievement: 120% of target ✓
-
-This is a significant win! I've also updated my personal notes with next steps for
-Acme's onboarding.
-
-Updated sales_knowledge block (excerpt):
-----------------------------------------------------------------------
-=== SALES KNOWLEDGE BASE ===
-Last Updated: 2024-10-08 15:30 PM
-
-RECENT WINS:
-- Acme Corp: $300K Enterprise deal (Sales Rep 1) - Oct 8, 2024
-
-CUSTOMER ACCOUNTS:
-- Total Active Customers: 248 (was 247)
-- Pipeline Value: $2.0M (was $2.3M, Acme converted)
-- Avg Deal Size: $15,100
-
-SALES TARGETS Q4:
-- Team Target: $1.5M in new revenue
-- Rep 1 Target: $600K → Actual: $720K (120% ✓)
-...
-----------------------------------------------------------------------
-
-Now Sales Director checks the shared sales_knowledge...
-User → Sales Director: 'Any wins from the team today?'
-
-Sales Director: Yes! Great news from the team:
-
-**🎉 Major Win: Acme Corp - $300K Enterprise Deal**
-Closed by: Sales Rep 1
-Date: Today (Oct 8)
-
-This is fantastic - Sales Rep 1 has now hit 120% of their Q4 target ($720K / $600K).
-This was one of our strategic accounts, and the $300K deal size is well above our
-average.
-
-**Team Status:**
-- Rep 1: $720K closed (120% of target) - exceeding expectations
-- Rep 2: $380K closed (63% of target) - on track
-- Team total: $1.1M / $1.5M (73%)
-
-We're in great shape for Q4. I should recognize Rep 1's performance and make sure
-we have a smooth onboarding for Acme.
-
-Analysis:
-- Sales Rep 1 updated shared sales_knowledge block
-- Sales Director immediately sees the update
-- Demonstrates bottom-up information flow within department
-- Director has visibility into team performance without explicit reporting
-```
-
-
-### Scenario 8: Information Cascade (Top-Down)
-
-```python
-print("\n" + "=" * 70)
-print("SCENARIO 8: CEO Decision Cascades Down")
-print("=" * 70)
-print("\nUser → CEO: 'The board wants us to prioritize the enterprise segment. Update the strategy and make sure all departments know.'\n")
-
-response = client.agents.messages.create(
- agent_id=ceo.id,
- messages=[{
- "role": "user",
- "content": "The board meeting went well. Key decision: We're doubling down on the enterprise segment. Update the executive dashboard and coordinate with department directors via the cross-department projects block."
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"CEO: {msg.content}\n")
-
-print("Now check if Directors see the update...")
-print("\nUser → Sales Director: 'Any strategic updates from leadership?'\n")
-
-response = client.agents.messages.create(
- agent_id=sales_director.id,
- messages=[{
- "role": "user",
- "content": "Check the cross-department projects block - any strategic updates from the CEO?"
- }]
-)
-
-for msg in response.messages:
- if msg.message_type == "assistant_message":
- print(f"Sales Director: {msg.content}\n")
-
-print("Analysis:")
-print("- CEO updated executive_dashboard (private) and cross_dept_projects (directors)")
-print("- Directors see strategic directive in cross_dept_projects")
-print("- CEO doesn't need to message each director individually")
-print("- Demonstrates top-down information cascade through shared blocks")
-```
-
-
-```
-======================================================================
-SCENARIO 8: CEO Decision Cascades Down
-======================================================================
-
-User → CEO: 'The board wants us to prioritize the enterprise segment...'
-
-CEO: I've updated our strategic direction based on the board meeting:
-
-**STRATEGIC PRIORITY UPDATE: Enterprise Segment Focus**
-
-Updated in executive_dashboard:
-- Strategic Priority #1: Enterprise market expansion (elevated priority)
-- Target: 5 new enterprise customers by EOY (was 3)
-- Investment: Increased focus on enterprise features
-
-Updated in cross_dept_projects:
-- Added strategic directive for all directors
-- Each department should align their work with enterprise focus
-
-**Department Implications:**
-- Sales: Prioritize enterprise pipeline, target larger deals
-- Engineering: Fast-track enterprise features, improve onboarding
-- HR: Hire profiles with enterprise SaaS experience
-
-I've coordinated this through cross_dept_projects so all directors are aligned.
-They can now cascade this down to their teams and update their department priorities
-accordingly.
-
-Now check if Directors see the update...
-
-User → Sales Director: 'Any strategic updates from leadership?'
-
-Sales Director: Yes, I see a significant strategic update from the CEO in the
-cross-department projects block:
-
-**NEW STRATEGIC PRIORITY: Enterprise Segment Focus**
-
-The CEO has communicated that we're doubling down on enterprise customers. This affects
-my sales strategy immediately:
-
-**Sales Implications:**
-✓ Align perfectly with our current focus (Acme Corp win today)
-✓ Prioritize enterprise pipeline ($2M+ deal sizes)
-✓ Adjust messaging and positioning for larger organizations
-✓ May need to shift resources from SMB deals to enterprise
-
-**Action Items for My Team:**
-1. Brief Rep 1 and Rep 2 on enterprise prioritization
-2. Review pipeline and identify enterprise opportunities
-3. Coordinate with Engineering on enterprise feature roadmap
-4. Update sales_knowledge with enterprise-focused playbook
-
-This is great strategic clarity from leadership. Our Enterprise Customer Onboarding
-project with Engineering just became even more critical.
-
-Analysis:
-- CEO updated executive_dashboard (private) and cross_dept_projects (directors)
-- Directors see strategic directive in cross_dept_projects
-- CEO doesn't need to message each director individually
-- Demonstrates top-down information cascade through shared blocks
-```
-
-
-## Part 4: Block Access Matrix and Summary
-
-```python
-print("\n" + "=" * 70)
-print("COMPREHENSIVE BLOCK ACCESS MATRIX")
-print("=" * 70)
-
-# Create comprehensive access report
-blocks_to_check = [
- (company_mission, "company_mission"),
- (company_policies, "company_policies"),
- (sales_knowledge, "sales_knowledge"),
- (engineering_specs, "engineering_specs"),
- (hr_employee_data, "hr_employee_data"),
- (cross_dept_projects, "cross_dept_projects"),
- (executive_dashboard, "executive_dashboard"),
-]
-
-for block, name in blocks_to_check:
- print(f"\n{name}:")
- block_info = client.blocks.retrieve(block_id=block.id)
- print(f" Agents with access: {len(block_info.agent_ids)}")
- for agent_id in block_info.agent_ids:
- agent_info = client.agents.retrieve(agent_id=agent_id)
- print(f" - {agent_info.name}")
-
-print("\n" + "=" * 70)
-print("TUTORIAL 4 COMPLETE: Key Takeaways")
-print("=" * 70)
-
-print("""
-✓ Created complete enterprise system with 10 agents across 4 organizational levels
-✓ Implemented 4-tier block hierarchy (company → department → cross-dept → executive)
-✓ Demonstrated department isolation (Sales/Eng/HR cannot see each other's data)
-✓ Showed cross-department coordination via cross_dept_projects
-✓ Demonstrated executive oversight via executive_dashboard
-✓ Showed bidirectional information flow (top-down and bottom-up)
-✓ Demonstrated HR confidentiality protection
-✓ Showed asynchronous coordination eliminating explicit messaging
-
-COMPLETE BLOCK ACCESS MATRIX:
-==============================
- company_ company_ sales_ engineering_ hr_ cross_ executive_
- mission policies knowledge specs employee_ dept_ dashboard
- data projects
-CEO R R - - - R/W R/W
-Sales Director R R R/W - - R/W -
-Sales Rep 1 R R R/W - - - -
-Sales Rep 2 R R R/W - - - -
-Engineering Director R R - R/W - R/W -
-Engineer 1 R R - R/W - - -
-Engineer 2 R R - R/W - - -
-HR Director R R - - R/W R/W -
-HR Rep 1 R R - - R/W - -
-
-R = Read-only | R/W = Read/Write | - = No Access
-
-ARCHITECTURAL PATTERNS DEMONSTRATED:
-====================================
-1. **Company-wide blocks** (read-only): Universal access, consistent messaging
-2. **Department isolation**: Each dept has private knowledge base
-3. **Cross-department coordination**: Directors collaborate via shared block
-4. **Executive aggregation**: CEO sees metrics without micromanaging
-5. **Hierarchical information flow**: Top-down (strategy) and bottom-up (reporting)
-6. **Confidentiality boundaries**: HR data strictly protected
-7. **Asynchronous coordination**: No explicit messaging needed between departments
-
-SCALING INSIGHTS:
-=================
-- 10 agents with 17 total blocks (7 shared + 10 private)
-- Each agent has 3-5 block access on average
-- Complex access patterns scale well
-- Department boundaries maintain security
-- Cross-functional work enabled without breaking isolation
-
-REAL-WORLD USE CASES:
-=====================
-✓ Enterprise organizational structures
-✓ Regulated industries (HIPAA, GDPR compliance via block isolation)
-✓ Multi-team product development
-✓ Customer support with specialized teams
-✓ Educational institutions (departments, admin)
-✓ Government agencies (clearance levels, department boundaries)
-
-COMPARISON ACROSS ALL TUTORIALS:
-=================================
-Tutorial 1: Hierarchical access (3 tiers, read-only knowledge)
-Tutorial 2: Uniform team access (task coordination, read/write)
-Tutorial 3: Overlapping access (5 specialists, mixed blocks)
-Tutorial 4: Organizational hierarchy (10 agents, 4-tier blocks, department isolation)
-
-KEY TAKEAWAY:
-=============
-Shared memory blocks enable enterprise-scale multi-agent systems with:
-- Appropriate information boundaries
-- Asynchronous coordination
-- Hierarchical oversight
-- Department autonomy
-- Cross-functional collaboration
-
-All without explicit agent-to-agent messaging or centralized control!
-""")
-```
-
-
-```
-======================================================================
-COMPREHENSIVE BLOCK ACCESS MATRIX
-======================================================================
-
-company_mission:
- Agents with access: 10
- - CEO
- - Sales_Director
- - Sales_Rep_1
- - Sales_Rep_2
- - Engineering_Director
- - Engineer_1
- - Engineer_2
- - HR_Director
- - HR_Rep_1
-
-company_policies:
- Agents with access: 10
- - CEO
- - Sales_Director
- - Sales_Rep_1
- - Sales_Rep_2
- - Engineering_Director
- - Engineer_1
- - Engineer_2
- - HR_Director
- - HR_Rep_1
-
-sales_knowledge:
- Agents with access: 3
- - Sales_Director
- - Sales_Rep_1
- - Sales_Rep_2
-
-engineering_specs:
- Agents with access: 3
- - Engineering_Director
- - Engineer_1
- - Engineer_2
-
-hr_employee_data:
- Agents with access: 2
- - HR_Director
- - HR_Rep_1
-
-cross_dept_projects:
- Agents with access: 4
- - CEO
- - Sales_Director
- - Engineering_Director
- - HR_Director
-
-executive_dashboard:
- Agents with access: 1
- - CEO
-
-======================================================================
-TUTORIAL 4 COMPLETE: Key Takeaways
-======================================================================
-... (full summary as shown above)
-```
-
-
-## Key Takeaways
-
-
-
-Company → Department → Cross-Dept → Executive blocks create organizational structure
-
-
-
-Each department maintains private knowledge bases invisible to other departments
-
-
-
-Directors coordinate via cross_dept_projects without explicit messaging
-
-
-
-CEO sees aggregated metrics and coordination without micromanaging
-
-
-
-### Architectural Patterns
-
-| Pattern | Description | Blocks |
-|---|---|---|
-| **Universal Access** | All agents see | `company_mission`, `company_policies` |
-| **Department Silos** | Department-only access | `sales_knowledge`, `engineering_specs`, `hr_employee_data` |
-| **Director Coordination** | Directors + CEO | `cross_dept_projects` |
-| **Executive Private** | CEO only | `executive_dashboard` |
-
-### Comparison Across All 4 Tutorials
-
-| Aspect | Tutorial 1 | Tutorial 2 | Tutorial 3 | Tutorial 4 |
-|---|---|---|---|---|
-| **Agents** | 4 | 4 | 5 | 10 |
-| **Shared Blocks** | 3 | 3 | 5 | 7 |
-| **Access Pattern** | Hierarchical tiers | Uniform team | Overlapping | Organizational hierarchy |
-| **Complexity** | Low | Medium | High | Very High |
-| **Read/Write Mix** | Read-only | Read/Write | Mixed | Mixed |
-| **Use Case** | Support tiers | Task coordination | Personal assistant | Enterprise org |
-
-## Series Conclusion
-
-You've now completed all 4 shared memory block tutorials! Here
-'s what you've learned:
-
-### Tutorial Progression
-
-**Part 1: Read-Only Knowledge** → Foundation
-- Hierarchical access control
-- Read-only blocks for policies
-- Information consistency
-
-**Part 2: Task Coordination** → Collaboration
-- Read/write shared blocks
-- Worker coordination patterns
-- Knowledge sharing
-
-**Part 3: User Assistant** → Specialization
-- Overlapping access patterns
-- Privacy boundaries
-- Zero-handoff workflows
-
-**Part 4: Enterprise System** → Scale
-- Organizational hierarchies
-- Department isolation
-- Cross-functional coordination
-- Executive oversight
-
-### Universal Principles
-
-Across all tutorials, these principles hold:
-
-1. **Appropriate Access**: Agents only access blocks they need (least privilege)
-2. **Real-Time Sync**: Updates to shared blocks immediately visible to all agents with access
-3. **No Explicit Coordination**: Shared blocks eliminate need for agent-to-agent messaging
-4. **Privacy Boundaries**: Sensitive data restricted to appropriate agents
-5. **Scalability**: Patterns work from 2 agents to 10+ agents
-
-## Next Steps
-
-
-
-Complete API documentation for blocks and memory management
-
-
-
-High-level overview of multi-agent system architectures
-
-
-
-Comprehensive guide to shared memory blocks (coming soon based on these tutorials!)
-
-
-
-Get help and share your multi-agent implementations
-
-
diff --git a/fern/pages/tutorials/letta-rag.mdx b/fern/pages/tutorials/letta-rag.mdx
deleted file mode 100644
index d4b9445b..00000000
--- a/fern/pages/tutorials/letta-rag.mdx
+++ /dev/null
@@ -1,471 +0,0 @@
----
-title: Connect Your Custom RAG Pipeline to a Letta Agent
-subtitle: A step-by-step guide to integrating external vector databases with Letta Cloud.
-slug: cookbooks/custom-rag-integration
----
-
-You've built a powerful Retrieval-Augmented Generation (RAG) pipeline with its own vector database, but now you want to connect it to an intelligent agent. This guide is for developers who want to integrate their existing RAG stack with Letta, giving them full control over their data while leveraging Letta's advanced agentic capabilities.
-
-By the end of this tutorial, we'll build a research assistant that uses a ChromaDB Cloud database to answer questions about scientific papers. We will explore two distinct methods for achieving this.
-
-### What You'll Learn
-
-- **Standard RAG:** How to manage retrieval on your client and inject context directly into the agent's prompt. This gives you maximum control over the data the agent sees.
-- **Agentic RAG:** How to empower your agent with a custom tool, allowing it to decide when and what to search in your vector database. This creates a more autonomous and flexible agent.
-
-## Prerequisites
-
-To follow along, you need free accounts for the following platforms:
-
-- **[Letta](https://www.letta.com):** To access the agent development platform
-- **[ChromaDB Cloud](https://www.trychroma.com/):** To host our vector database
-
-You will also need Python 3.8+ and a code editor.
-
-### Getting Your API Keys
-
-We'll need two API keys for this tutorial.
-
-
-
-
-
- If you don't have one, sign up for a free account at [letta.com](https://www.letta.com).
-
-
- Once logged in, click on **API keys** in the sidebar.
- 
-
-
- Click **+ Create API key**, give it a descriptive name, and click **Confirm**. Copy the key and save it somewhere safe.
-
-
-
-
-
-
-
- Sign up for a free account on the [ChromaDB Cloud website](https://www.trychroma.com/).
-
-
- From your dashboard, create a new database.
- 
-
-
- In your project settings, you will find your **API Key** and **Host URL**. We'll need both of these for our scripts.
- 
-
-
-
-
-
-Once you have these keys, create a `.env` file in your project directory and add them like this:
-
-```
-LETTA_API_KEY="..."
-CHROMA_API_KEY="..."
-CHROMA_TENANT="..."
-CHROMA_DATABASE="..."
-```
-
-## Part 1: Standard RAG — Full Control on the Client-Side
-
-In the standard RAG approach, our application takes the lead. It fetches the relevant information from our ChromaDB database and then passes this context, along with our query, to a simple Letta agent. This method is direct, transparent, and keeps all the retrieval logic in our client application.
-
-### Step 1: Set Up the Cloud Vector Database
-
-First, we need to populate our ChromaDB Cloud database with the content of the research papers. We'll use two papers for this demo: ["Attention Is All You Need"](https://arxiv.org/abs/1706.03762) and ["BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding"](https://arxiv.org/abs/1810.04805).
-
-Before we begin, let's create a Python virtual environment to keep our dependencies isolated:
-
-```shell
-python -m venv venv
-source venv/bin/activate # On Windows, use: venv\Scripts\activate
-```
-
-Download the research papers we'll be using:
-
-```shell
-curl -o 1706.03762.pdf https://arxiv.org/pdf/1706.03762.pdf
-curl -o 1810.04805.pdf https://arxiv.org/pdf/1810.04805.pdf
-```
-
-Now, create a `requirements.txt` file with the necessary Python libraries:
-
-```
-letta-client
-chromadb
-pypdf
-python-dotenv
-```
-
-Install them using pip:
-
-```shell
-pip install -r requirements.txt
-```
-
-Now, create a `setup.py` file. This script will load the PDFs, split them into manageable chunks, and ingest them into a ChromaDB collection named `rag_collection`.
-
-```python
-import os
-import chromadb
-import pypdf
-from dotenv import load_dotenv
-
-load_dotenv()
-
-def main():
- # Connect to ChromaDB Cloud
- client = chromadb.CloudClient(
- tenant=os.getenv("CHROMA_TENANT"),
- database=os.getenv("CHROMA_DATABASE"),
- api_key=os.getenv("CHROMA_API_KEY")
- )
-
- # Create or get the collection
- collection = client.get_or_create_collection("rag_collection")
-
- # Ingest PDFs
- pdf_files = ["1706.03762.pdf", "1810.04805.pdf"]
- for pdf_file in pdf_files:
- print(f"Ingesting {pdf_file}...")
- reader = pypdf.PdfReader(pdf_file)
- for i, page in enumerate(reader.pages):
- collection.add(
- ids=[f"{pdf_file}-{i}"],
- documents=[page.extract_text()]
- )
-
- print("\nIngestion complete!")
- print(f"Total documents in collection: {collection.count()}")
-
-if __name__ == "__main__":
- main()
-
-```
-
-Run the script from your terminal:
-
-```shell
-python setup.py
-```
-
-This script connects to your ChromaDB Cloud instance, creates a collection, and adds the text content of each page from the PDFs as a separate document. Your vector database is now ready.
-
-### Step 2: Create a "Stateless" Letta Agent
-
-For the standard RAG approach, the Letta agent doesn't need any special tools or complex instructions. Its only job is to answer a question based on the context we provide. We can create this agent programmatically using the Letta SDK.
-
-Create a file named `create_agent.py`:
-
-```python
-import os
-from letta_client import Letta
-from dotenv import load_dotenv
-
-load_dotenv()
-
-# Initialize the Letta client
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# Create the agent
-agent = client.agents.create(
- name="Stateless RAG Agent",
- description="This agent answers questions based on provided context. It has no tools or special memory.",
- memory_blocks=[
- {
- "label": "persona",
- "value": "You are a helpful research assistant. Answer the user's question based *only* on the context provided."
- }
- ]
-)
-
-print(f"Agent '{agent.name}' created with ID: {agent.id}")
-
-```
-
-Run this script once to create the agent in your Letta project.
-
-```shell
-python create_agent.py
-```
-
-
-
-### Step 3: Query, Format, and Ask
-
-Now we'll write the main script, `standard_rag.py`, that ties everything together. This script will:
-
-1. Take a user's question.
-2. Query the `rag-demo` collection in ChromaDB to find the most relevant document chunks.
-3. Construct a detailed prompt that includes both the user's question and the retrieved context.
-4. Send this combined prompt to our stateless Letta agent and print the response.
-
-```python
-import os
-import chromadb
-from letta_client import Letta
-from dotenv import load_dotenv
-
-load_dotenv()
-
-# Initialize clients
-letta_client = Letta(token=os.getenv("LETTA_API_KEY"))
-chroma_client = chromadb.CloudClient(
- tenant=os.getenv("CHROMA_TENANT"),
- database=os.getenv("CHROMA_DATABASE"),
- api_key=os.getenv("CHROMA_API_KEY")
-)
-
-AGENT_ID = "your-stateless-agent-id" # Replace with your agent ID
-
-def main():
- while True:
- question = input("Ask a question about the research papers: ")
- if question.lower() in ['exit', 'quit']:
- break
-
- # 1. Query ChromaDB
- collection = chroma_client.get_collection("rag_collection")
- results = collection.query(query_texts=[question], n_results=3)
- context = "\n".join(results["documents"][0])
-
- # 2. Construct the prompt
- prompt = f'''Context from research paper:
-{context}
-Question: {question}
-Answer:'''
-
- # 3. Send to Letta Agent
- response = letta_client.agents.messages.create(
- agent_id=AGENT_ID,
- messages=[{"role": "user", "content": prompt}]
- )
-
- for message in response.messages:
- if message.message_type == 'assistant_message':
- print(f"Agent: {message.content}")
-
-if __name__ == "__main__":
- main()
-
-```
-
-
-Replace `your-stateless-agent-id` with the actual ID of the agent you created in the previous step.
-
-
-When you run this script, your application performs the retrieval, and the Letta agent simply provides the answer based on the context it receives. This gives you full control over the data pipeline.
-
-## Part 2: Agentic RAG — Empowering Your Agent with Tools
-
-In the agentic RAG approach, we delegate the retrieval process to the agent itself. Instead of our application deciding what to search for, we provide the agent with a custom tool that allows it to query our ChromaDB database directly. This makes the agent more autonomous and our client-side code much simpler.
-
-### Step 4: Create a Custom Search Tool
-
-A Letta tool is essentially a Python function that your agent can call. We'll create a function that searches our ChromaDB collection and returns the results. Letta handles the complexities of exposing this function to the agent securely.
-
-Create a new file named `tools.py`:
-
-```python
-import chromadb
-import os
-
-def search_research_papers(query_text: str, n_results: int = 1) -> str:
- """
- Searches the research paper collection for a given query.
- Args:
- query_text (str): The text to search for.
- n_results (int): The number of results to return.
- Returns:
- str: The most relevant document found.
- """
- # ChromaDB Cloud Client
- # This tool code is executed on the Letta server. It expects the ChromaDB
- # credentials to be passed as environment variables.
- api_key = os.getenv("CHROMA_API_KEY")
- tenant = os.getenv("CHROMA_TENANT")
- database = os.getenv("CHROMA_DATABASE")
-
- if not all([api_key, tenant, database]):
- # If run locally without the env vars, this will fail early.
- # When run by the agent, these will be provided by the tool execution environment.
- raise ValueError("CHROMA_API_KEY, CHROMA_TENANT, and CHROMA_DATABASE must be set as environment variables.")
-
- client = chromadb.CloudClient(
- tenant=tenant,
- database=database,
- api_key=api_key
- )
-
- collection = client.get_or_create_collection("rag_collection")
-
- try:
- results = collection.query(
- query_texts=[query_text],
- n_results=n_results
- )
-
- document = results['documents'][0][0]
- return document
- except Exception as e:
- return f"Tool failed with error: {e}"
-
-```
-
-This function, `search_research_papers`, takes a query, connects to our database, retrieves the top three most relevant documents, and returns them as a single string.
-
-### Step 5: Configure a "Smart" Research Agent
-
-Next, we'll create a new, more advanced agent. This agent will have a specific persona that instructs it on how to behave and, most importantly, it will be equipped with our new search tool.
-
-Create a file named `create_agentic_agent.py`:
-
-```python
-import os
-from letta_client import Letta
-from dotenv import load_dotenv
-from tools import search_research_papers
-
-load_dotenv()
-
-# Initialize the Letta client
-client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-# Create a tool from our Python function
-search_tool = client.tools.create_from_function(func=search_research_papers)
-
-# Define the agent's persona
-persona = """You are a world-class research assistant. Your goal is to answer questions accurately by searching through a database of research papers. When a user asks a question, first use the `search_research_papers` tool to find relevant information. Then, answer the user's question based on the information returned by the tool."""
-
-# Create the agent with the tool attached
-agent = client.agents.create(
- name="Agentic RAG Assistant",
- description="A smart agent that can search a vector database to answer questions.",
- memory_blocks=[
- {
- "label": "persona",
- "value": persona
- }
- ],
- tools=[search_tool.name]
-)
-
-print(f"Agent '{agent.name}' created with ID: {agent.id}")
-
-```
-
-Run this script to create the agent:
-
-```shell
-python create_agentic_agent.py
-```
-
-#### Configure Tool Dependencies and Environment Variables
-
-For the tool to work within Letta's environment, we need to configure its dependencies and environment variables through the Letta dashboard.
-
-
-
- Navigate to your Letta dashboard and find the "Agentic RAG Assistant" agent you just created.
-
-
- Click on your agent to open the Agent Development Environment (ADE).
-
-
- - In the ADE, select **Tools** from the sidebar
- - Find and click on the `search_research_papers` tool
- - Click on the **Dependencies** tab
- - Add `chromadb` as a dependency
-
- 
-
-
- - In the same tool configuration, navigate to **Simulator** > **Environment**
- - Add the following environment variables with their corresponding values from your `.env` file:
- - `CHROMA_API_KEY`
- - `CHROMA_TENANT`
- - `CHROMA_DATABASE`
-
- 
-
-
-
-Now, when the agent calls this tool, Letta's execution environment will know to install `chromadb` and will have access to the necessary credentials to connect to your database.
-
-### Step 6: Let the Agent Lead the Conversation
-
-With the agentic setup, our client-side code becomes incredibly simple. We no longer need to worry about retrieving context; we just send the user's raw question to the agent and let it handle the rest.
-
-Create the `agentic_rag.py` script:
-
-```python
-import os
-from letta_client import Letta
-from dotenv import load_dotenv
-
-load_dotenv()
-
-# Initialize client
-letta_client = Letta(token=os.getenv("LETTA_API_KEY"))
-
-AGENT_ID = "your-agentic-agent-id" # Replace with your new agent ID
-
-def main():
- while True:
- user_query = input("Ask a question about the research papers: ")
- if user_query.lower() in ['exit', 'quit']:
- break
-
- response = letta_client.agents.messages.create(
- agent_id=AGENT_ID,
- messages=[{"role": "user", "content": user_query}]
- )
-
- for message in response.messages:
- if message.message_type == 'assistant_message':
- print(f"Agent: {message.content}")
-
-if __name__ == "__main__":
- main()
-
-```
-
-
-Replace `your-agentic-agent-id` with the ID of the new agent you just created.
-
-
-When you run this script, the agent receives the question, understands from its persona that it needs to search for information, calls the `search_research_papers` tool, gets the context, and then formulates an answer. All the RAG logic is handled by the agent, not your application.
-
-## Which Approach Is Right for You?
-
-We've explored two powerful methods for connecting a custom RAG pipeline to a Letta agent. The best choice depends on your specific needs.
-
-- **Use Standard RAG when...**
- - You want to maintain complete, fine-grained control over the retrieval process.
- - Your retrieval logic is complex and better handled by your application code.
- - You want to keep your agent as simple as possible and minimize its autonomy.
-
-- **Use Agentic RAG when...**
- - You want to build a more autonomous agent that can handle complex, multi-step queries.
- - You prefer simpler, cleaner client-side code.
- - You want the agent to decide *when* and *what* to search for, leading to more dynamic conversations.
-
-## What's Next?
-
-Now that you've integrated a custom RAG pipeline, you can expand on this foundation. Here are a few ideas:
-
-
-
-Swap out ChromaDB for other providers like Weaviate, Pinecone, or a database you already have in production. The core logic remains the same: create a tool that queries your database and equip your agent with it.
-
-
-
-Create tools that not only read from your database but also write new information to it. This would allow your agent to learn from its interactions and update its own knowledge base over time.
-
-
-
-Expand your RAG pipeline to include more documents, web pages, or other sources of information. The more comprehensive your data source, the more capable your agent will become.
-
-
diff --git a/fern/pages/voice/voice.mdx b/fern/pages/voice/voice.mdx
deleted file mode 100644
index 0ca3aaa6..00000000
--- a/fern/pages/voice/voice.mdx
+++ /dev/null
@@ -1,35 +0,0 @@
----
-title: Voice Agents
-slug: guides/voice/overview
----
-
-
-Voice agents support is experimental and may be unstable. For more information, visit our [Discord](https://discord.gg/letta).
-
-
-All Letta agents can be connected to a voice provider by using the OpenAI-compatible streaming chat completions endpoint at `https://api.letta.com/v1/chat/completions`. Any standard Letta agent can be used for voice applications.
-
-
-The legacy `/v1/voice-beta/` endpoint has been deprecated. Please use the OpenAI-compatible `/v1/chat/completions` endpoint with `stream=true` for voice applications.
-
-
-## Creating a voice agent
-You can create a voice agent using the standard Letta agent creation flow:
-
-```python
-from letta_client import Letta
-import os
-
-client = Letta(token=os.getenv('LETTA_API_KEY'))
-
-# create the Letta agent
-agent = client.agents.create(
- memory_blocks=[
- {"value": "Name: ?", "label": "human"},
- {"value": "You are a helpful assistant.", "label": "persona"},
- ],
- model="openai/gpt-4o-mini" # Use 4o-mini for speed
-)
-```
-
-You can attach additional tools and blocks to this agent just as you would any other Letta agent.
diff --git a/fern/project.json b/fern/project.json
deleted file mode 100644
index c51d0155..00000000
--- a/fern/project.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "name": "docs",
- "$schema": "../../../node_modules/nx/schemas/project-schema.json",
- "sourceRoot": "apps/core/fern",
- "projectType": "application",
- "tags": [],
- "targets": {
- "dev": {
- "executor": "nx:run-commands",
- "options": {
- "cwd": "apps/core/fern",
- "command": "fern docs dev"
- }
- },
- "generate-openapi": {
- "executor": "nx:run-commands",
- "options": {
- "cwd": "apps/core/fern",
- "command": "ts-node ./scripts/prepare-openapi.ts"
- }
- }
- }
-}
diff --git a/fern/tsconfig.json b/fern/tsconfig.json
deleted file mode 100644
index 8d020740..00000000
--- a/fern/tsconfig.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "extends": "../../../tsconfig.base.json",
- "compilerOptions": {
- "target": "es2020",
- "module": "esnext",
- "lib": ["esnext"],
- "types": ["node"],
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "moduleResolution": "node",
- "resolveJsonModule": true,
- "allowSyntheticDefaultImports": true,
- "noEmit": true
- },
- "include": ["scripts/**/*.ts"],
- "exclude": ["node_modules"]
-}