commit 4deafc5b1edd3a5ca2036ec2e37b4b167222af31 Author: wehub-resource-sync Date: Mon Jul 13 12:40:25 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ffe57a7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +**/__pycache__ +**/.pytest_cache +**/*.pyc +**/*.pyo +**/*.pyd +.git +.gitignore +.env +*.log diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c788793 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +########################################################## +Example enviornment variable configurations for the Letta +Docker container. Un-coment the sections you want to +configure with. +########################################################## + + +########################################################## + OpenAI configuration +########################################################## +# OPENAI_API_KEY=sk-... + +########################################################## + Ollama configuration +########################################################## +# OLLAMA_BASE_URL="http://host.docker.internal:11434" + +########################################################## + vLLM configuration +########################################################## +# VLLM_API_BASE="http://host.docker.internal:8000" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..108cb3b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,20 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to LF on checkout. +*.py text eol=lf +*.txt text eol=lf +*.md text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.yaml text eol=lf + +# Declare files that will always have CRLF line endings on checkout. +# (Only if you have specific Windows-only files) +*.bat text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.gif binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..73900ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,101 @@ +name: Bug Report +description: Create a report to help us improve +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + > **Reporting a bug with Letta Code?** + > Please file your issue at **[letta-ai/letta-code](https://github.com/letta-ai/letta-code/issues)** instead! + > This repository is for the core Letta server only. + + - type: checkboxes + id: ai-disclosure + attributes: + label: AI Disclosure + description: | + We require all issue authors to disclose AI usage per our [AI Policy](https://github.com/letta-ai/letta/blob/main/AI_POLICY.md). + Check **all** that apply. + options: + - label: This issue was written entirely by a human + - label: This issue was written with AI assistance (e.g. Copilot, ChatGPT, Claude) and **reviewed and edited by a human** + - label: I have read the [AI Policy](https://github.com/letta-ai/letta/blob/main/AI_POLICY.md) and agree to its terms + required: true + validations: + required: true + + - type: textarea + id: human-verification + attributes: + label: Human Verification + description: | + To help us combat spam, please copy and paste the following phrase **exactly** into the box below: + `I have read the AI policy and I confirm this issue was reviewed by a human.` + placeholder: "I have read the AI policy and I confirm this issue was reviewed by a human." + validations: + required: true + + - type: textarea + id: description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + placeholder: What happened? What did you expect to happen? + validations: + required: true + + - type: dropdown + id: deployment + attributes: + label: How are you running Letta? + options: + - Docker + - From source + - Desktop + - pip (legacy) + validations: + required: true + + - type: input + id: os + attributes: + label: Operating System + placeholder: e.g. macOS 15.4, Ubuntu 24.04, Windows 11 + + - type: input + id: version + attributes: + label: Letta Version + description: Run `letta version` or check your Docker image tag. + placeholder: e.g. 0.16.7 + + - type: input + id: model + attributes: + label: Model + description: Which LLM model are you using? + placeholder: e.g. gpt-4o, claude-sonnet-4-20250514, local llama + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: How can we reproduce this issue? + placeholder: | + 1. Start Letta server with ... + 2. Create an agent with ... + 3. Send message ... + 4. See error ... + + - type: textarea + id: logs + attributes: + label: Relevant Logs / Screenshots + description: Paste any relevant log output, error messages, or screenshots. + render: shell + + - type: textarea + id: context + attributes: + label: Additional Context + description: Any other context about the problem. Attach `.af` agent files if applicable. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..371e926 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Letta Code Issues + url: https://github.com/letta-ai/letta-code/issues + about: For bugs and feature requests related to Letta Code (the CLI tool). + - name: Discord + url: https://discord.gg/9GEQrxmVyE + about: Chat with the Letta community and get help. + - name: Documentation + url: https://docs.letta.com + about: Check the docs before filing an issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..a63104d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,65 @@ +name: Feature Request +description: Suggest an idea for this project +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + > **Requesting a feature for Letta Code?** + > Please file your issue at **[letta-ai/letta-code](https://github.com/letta-ai/letta-code/issues)** instead! + > This repository is for the core Letta server only. + + - type: checkboxes + id: ai-disclosure + attributes: + label: AI Disclosure + description: | + We require all issue authors to disclose AI usage per our [AI Policy](https://github.com/letta-ai/letta/blob/main/AI_POLICY.md). + Check **all** that apply. + options: + - label: This issue was written entirely by a human + - label: This issue was written with AI assistance (e.g. Copilot, ChatGPT, Claude) and **reviewed and edited by a human** + - label: I have read the [AI Policy](https://github.com/letta-ai/letta/blob/main/AI_POLICY.md) and agree to its terms + required: true + validations: + required: true + + - type: textarea + id: human-verification + attributes: + label: Human Verification + description: | + To help us combat spam, please copy and paste the following phrase **exactly** into the box below: + `I have read the AI policy and I confirm this issue was reviewed by a human.` + placeholder: "I have read the AI policy and I confirm this issue was reviewed by a human." + validations: + required: true + + - type: textarea + id: problem + attributes: + label: Problem Statement + description: Is your feature request related to a problem? Describe it clearly. + placeholder: I'm always frustrated when [...] + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Any alternative solutions or features you've considered. + + - type: textarea + id: context + attributes: + label: Additional Context + description: Any other context or screenshots about the feature request. diff --git a/.github/TRUSTED_CONTRIBUTORS b/.github/TRUSTED_CONTRIBUTORS new file mode 100644 index 0000000..00276ae --- /dev/null +++ b/.github/TRUSTED_CONTRIBUTORS @@ -0,0 +1,13 @@ +# Trusted Contributors +# +# Users listed here bypass issue validation checks (AI disclosure, +# human verification phrase). One GitHub username per line. +# +# Note: Members of the letta-ai GitHub organization are automatically +# trusted and do not need to be listed here. This file is for +# non-org contributors who have earned trust. +# +# To add someone, open a PR adding their username to this list. + +# Letta community contributors +ezra-letta diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..8035af3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,17 @@ +**Please describe the purpose of this pull request.** +Is it to add a new feature? Is it to fix a bug? + +**How to test** +How can we test your PR during review? What commands should we run? What outcomes should we expect? + +**Have you tested this PR?** +Have you tested the latest commit on the PR? If so please provide outputs from your tests. + +**Related issues or PRs** +Please link any related GitHub [issues](https://github.com/letta-ai/letta/issues) or [PRs](https://github.com/letta-ai/letta/pulls). + +**Is your PR over 500 lines of code?** +If so, please break up your PR into multiple smaller PRs so that we can review them quickly, or provide justification for its length. + +**Additional context** +Add any other context or screenshots about the PR here. diff --git a/.github/scripts/model-sweep/conftest.py b/.github/scripts/model-sweep/conftest.py new file mode 100644 index 0000000..edc0ae3 --- /dev/null +++ b/.github/scripts/model-sweep/conftest.py @@ -0,0 +1,278 @@ +import logging +import os +import socket +import threading +import time +from datetime import datetime, timezone +from typing import Generator + +import pytest +import requests +from anthropic.types.beta.messages import BetaMessageBatch, BetaMessageBatchRequestCounts +from dotenv import load_dotenv +from letta_client import AsyncLetta, Letta + +from letta.schemas.agent import AgentState +from letta.schemas.llm_config import LLMConfig +from letta.services.organization_manager import OrganizationManager +from letta.services.user_manager import UserManager + + +def pytest_configure(config): + logging.basicConfig(level=logging.DEBUG) + + +@pytest.fixture +def disable_e2b_api_key() -> Generator[None, None, None]: + """ + Temporarily disables the E2B API key by setting `tool_settings.e2b_api_key` to None + for the duration of the test. Restores the original value afterward. + """ + from letta.settings import tool_settings + + original_api_key = tool_settings.e2b_api_key + tool_settings.e2b_api_key = None + yield + tool_settings.e2b_api_key = original_api_key + + +@pytest.fixture +def check_e2b_key_is_set(): + from letta.settings import tool_settings + + original_api_key = tool_settings.e2b_api_key + assert original_api_key is not None, "Missing e2b key! Cannot execute these tests." + yield + + +@pytest.fixture +def default_organization(): + """Fixture to create and return the default organization.""" + manager = OrganizationManager() + org = manager.create_default_organization() + yield org + + +@pytest.fixture +def default_user(default_organization): + """Fixture to create and return the default user within the default organization.""" + manager = UserManager() + user = manager.create_default_user(org_id=default_organization.id) + yield user + + +# --- Tool Fixtures --- +@pytest.fixture +def weather_tool_func(): + def get_weather(location: str) -> str: + """ + Fetches the current weather for a given location. + + Parameters: + location (str): The location to get the weather for. + + Returns: + str: A formatted string describing the weather in the given location. + + Raises: + RuntimeError: If the request to fetch weather data fails. + """ + import requests + + url = f"https://wttr.in/{location}?format=%C+%t" + + response = requests.get(url) + if response.status_code == 200: + weather_data = response.text + return f"The weather in {location} is {weather_data}." + else: + raise RuntimeError(f"Failed to get weather data, status code: {response.status_code}") + + yield get_weather + + +@pytest.fixture +def print_tool_func(): + """Fixture to create a tool with default settings and clean up after the test.""" + + def print_tool(message: str): + """ + Args: + message (str): The message to print. + + Returns: + str: The message that was printed. + """ + print(message) + return message + + yield print_tool + + +@pytest.fixture +def roll_dice_tool_func(): + def roll_dice(): + """ + Rolls a 6 sided die. + + Returns: + str: The roll result. + """ + import time + + time.sleep(1) + return "Rolled a 10!" + + yield roll_dice + + +@pytest.fixture +def dummy_beta_message_batch() -> BetaMessageBatch: + return BetaMessageBatch( + id="msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", + archived_at=datetime(2024, 8, 20, 18, 37, 24, 100435, tzinfo=timezone.utc), + cancel_initiated_at=datetime(2024, 8, 20, 18, 37, 24, 100435, tzinfo=timezone.utc), + created_at=datetime(2024, 8, 20, 18, 37, 24, 100435, tzinfo=timezone.utc), + ended_at=datetime(2024, 8, 20, 18, 37, 24, 100435, tzinfo=timezone.utc), + expires_at=datetime(2024, 8, 20, 18, 37, 24, 100435, tzinfo=timezone.utc), + processing_status="in_progress", + request_counts=BetaMessageBatchRequestCounts( + canceled=10, + errored=30, + expired=10, + processing=100, + succeeded=50, + ), + results_url="https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results", + type="message_batch", + ) + + +# --- Model Sweep --- +# Global flag to track server state +_server_started = False +_server_url = None + + +def _start_server_once() -> str: + """Start server exactly once, return URL""" + global _server_started, _server_url + + if _server_started and _server_url: + return _server_url + + url = os.getenv("LETTA_SERVER_URL", "http://localhost:8283") + + # Check if already running + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + if s.connect_ex(("localhost", 8283)) == 0: + _server_started = True + _server_url = url + return url + + # Start server (your existing logic) + if not os.getenv("LETTA_SERVER_URL"): + + def _run_server(): + load_dotenv() + from letta.server.rest_api.app import start_server + + start_server(debug=True) + + thread = threading.Thread(target=_run_server, daemon=True) + thread.start() + + # Poll until up + timeout_seconds = 60 + deadline = time.time() + timeout_seconds + while time.time() < deadline: + try: + resp = requests.get(url + "/v1/health") + if resp.status_code < 500: + break + except requests.exceptions.RequestException: + pass + time.sleep(0.1) + else: + raise RuntimeError(f"Could not reach {url} within {timeout_seconds}s") + + _server_started = True + _server_url = url + return url + + +# ------------------------------ +# Fixtures +# ------------------------------ + + +@pytest.fixture(scope="module") +def server_url() -> str: + """Return URL of already-started server""" + return _start_server_once() + + +@pytest.fixture(scope="module") +def client(server_url: str) -> Letta: + """ + Creates and returns a synchronous Letta REST client for testing. + """ + client_instance = Letta(base_url=server_url) + yield client_instance + + +@pytest.fixture(scope="function") +def async_client(server_url: str) -> AsyncLetta: + """ + Creates and returns an asynchronous Letta REST client for testing. + """ + async_client_instance = AsyncLetta(base_url=server_url) + yield async_client_instance + + +@pytest.fixture(scope="module") +def agent_state(client: Letta) -> AgentState: + """ + Creates and returns an agent state for testing with a pre-configured agent. + The agent is named 'supervisor' and is configured with base tools and the roll_dice tool. + """ + client.tools.upsert_base_tools() + + send_message_tool = client.tools.list(name="send_message")[0] + agent_state_instance = client.agents.create( + name="supervisor", + include_base_tools=False, + tool_ids=[send_message_tool.id], + model="openai/gpt-4o", + embedding="letta/letta-free", + tags=["supervisor"], + ) + yield agent_state_instance + + client.agents.delete(agent_state_instance.id) + + +@pytest.fixture(scope="module") +def all_available_llm_configs(client: Letta) -> [LLMConfig]: + """ + Returns a list of all available LLM configs. + """ + llm_configs = client.models.list() + return llm_configs + + +# create a client to the started server started at +def get_available_llm_configs() -> [LLMConfig]: + """Get configs, starting server if needed""" + server_url = _start_server_once() + temp_client = Letta(base_url=server_url) + return temp_client.models.list() + + +# dynamically insert llm_config paramter at collection time +def pytest_generate_tests(metafunc): + """Dynamically parametrize tests that need llm_config.""" + if "llm_config" in metafunc.fixturenames: + configs = get_available_llm_configs() + if configs: + metafunc.parametrize("llm_config", configs, ids=[c.model for c in configs]) diff --git a/.github/scripts/model-sweep/feature_mappings.json b/.github/scripts/model-sweep/feature_mappings.json new file mode 100644 index 0000000..41222e1 --- /dev/null +++ b/.github/scripts/model-sweep/feature_mappings.json @@ -0,0 +1,21 @@ +{ + "Basic": [ + "test_greeting_with_assistant_message", + "test_greeting_without_assistant_message", + "test_async_greeting_with_assistant_message", + "test_agent_loop_error", + "test_step_stream_agent_loop_error", + "test_step_streaming_greeting_with_assistant_message", + "test_step_streaming_greeting_without_assistant_message", + "test_step_streaming_tool_call", + "test_tool_call", + "test_auto_summarize" + ], + "Token Streaming": [ + "test_token_streaming_greeting_with_assistant_message", + "test_token_streaming_greeting_without_assistant_message", + "test_token_streaming_agent_loop_error", + "test_token_streaming_tool_call" + ], + "Multimodal": ["test_base64_image_input", "test_url_image_input"] +} diff --git a/.github/scripts/model-sweep/generate_model_sweep_markdown.py b/.github/scripts/model-sweep/generate_model_sweep_markdown.py new file mode 100644 index 0000000..c82e051 --- /dev/null +++ b/.github/scripts/model-sweep/generate_model_sweep_markdown.py @@ -0,0 +1,495 @@ +#!/usr/bin/env python3 +import json +import os +import sys +from collections import defaultdict +from datetime import datetime + + +def load_feature_mappings(config_file=None): + """Load feature mappings from config file.""" + if config_file is None: + # Default to feature_mappings.json in the same directory as this script + script_dir = os.path.dirname(os.path.abspath(__file__)) + config_file = os.path.join(script_dir, "feature_mappings.json") + + try: + with open(config_file, "r") as f: + return json.load(f) + except FileNotFoundError: + print(f"Error: Could not find feature mappings config file '{config_file}'") + sys.exit(1) + except json.JSONDecodeError: + print(f"Error: Invalid JSON in feature mappings config file '{config_file}'") + sys.exit(1) + + +def get_support_status(passed_tests, feature_tests): + """Determine support status for a feature category.""" + if not feature_tests: + return "❓" # Unknown - no tests for this feature + + # Filter out error tests when checking for support + non_error_tests = [test for test in feature_tests if not test.endswith("_error")] + [test for test in feature_tests if test.endswith("_error")] + + # Check which non-error tests passed + passed_non_error_tests = [test for test in non_error_tests if test in passed_tests] + + # If there are no non-error tests, only error tests, treat as unknown + if not non_error_tests: + return "❓" # Only error tests available + + # Support is based only on non-error tests + if len(passed_non_error_tests) == len(non_error_tests): + return "✅" # Full support + elif len(passed_non_error_tests) == 0: + return "❌" # No support + else: + return "⚠️" # Partial support + + +def categorize_tests(all_test_names, feature_mapping): + """Categorize test names into feature buckets.""" + categorized = {feature: [] for feature in feature_mapping.keys()} + + for test_name in all_test_names: + for feature, test_patterns in feature_mapping.items(): + if test_name in test_patterns: + categorized[feature].append(test_name) + break + + return categorized + + +def calculate_support_score(feature_support, feature_order): + """Calculate a numeric support score for ranking models. + + For partial support, the score is weighted by the position of the feature + in the feature_order list (earlier features get higher weight). + """ + score = 0 + max_features = len(feature_order) + + for feature, status in feature_support.items(): + # Get position weight (earlier features get higher weight) + if feature in feature_order: + position_weight = (max_features - feature_order.index(feature)) / max_features + else: + position_weight = 0.5 # Default weight for unmapped features + + if status == "✅": # Full support + score += 10 * position_weight + elif status == "⚠️": # Partial support - weighted by column position + score += 5 * position_weight + elif status == "❌": # No support + score += 1 * position_weight + # Unknown (❓) gets 0 points + return score + + +def calculate_provider_support_score(models_data, feature_order): + """Calculate a provider-level support score based on all models' support scores.""" + if not models_data: + return 0 + + # Calculate the average support score across all models in the provider + total_score = sum(model["support_score"] for model in models_data) + return total_score / len(models_data) + + +def get_test_function_line_numbers(test_file_path): + """Extract line numbers for test functions from the test file.""" + test_line_numbers = {} + + try: + with open(test_file_path, "r") as f: + lines = f.readlines() + + for i, line in enumerate(lines, 1): + if "def test_" in line and line.strip().startswith("def test_"): + # Extract function name + func_name = line.strip().split("def ")[1].split("(")[0] + test_line_numbers[func_name] = i + except FileNotFoundError: + print(f"Warning: Could not find test file at {test_file_path}") + + return test_line_numbers + + +def get_github_repo_info(): + """Get GitHub repository information from git remote.""" + try: + # Try to get the GitHub repo URL from git remote + import subprocess + + result = subprocess.run(["git", "remote", "get-url", "origin"], capture_output=True, text=True, cwd=os.path.dirname(__file__)) + if result.returncode == 0: + remote_url = result.stdout.strip() + # Parse GitHub URL + if "github.com" in remote_url: + if remote_url.startswith("https://"): + # https://github.com/user/repo.git -> user/repo + repo_path = remote_url.replace("https://github.com/", "").replace(".git", "") + elif remote_url.startswith("git@"): + # git@github.com:user/repo.git -> user/repo + repo_path = remote_url.split(":")[1].replace(".git", "") + else: + return None + return repo_path + except Exception: + pass + + # Default fallback + return "letta-ai/letta" + + +def generate_test_details(model_info, feature_mapping): + """Generate detailed test results for a model.""" + details = [] + + # Get test function line numbers + script_dir = os.path.dirname(os.path.abspath(__file__)) + test_file_path = os.path.join(script_dir, "model_sweep.py") + test_line_numbers = get_test_function_line_numbers(test_file_path) + + # Use the main branch GitHub URL + base_github_url = "https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py" + + for feature, tests in model_info["categorized_tests"].items(): + if not tests: + continue + + details.append(f"### {feature}") + details.append("") + + for test in sorted(tests): + if test in model_info["passed_tests"]: + status = "✅" + elif test in model_info["failed_tests"]: + status = "❌" + else: + status = "❓" + + # Create GitHub link if we have line number info + if test in test_line_numbers: + line_num = test_line_numbers[test] + github_link = f"{base_github_url}#L{line_num}" + details.append(f"- {status} [`{test}`]({github_link})") + else: + details.append(f"- {status} `{test}`") + details.append("") + + return details + + +def calculate_column_widths(all_provider_data, feature_mapping): + """Calculate the maximum width needed for each column across all providers.""" + widths = {"model": len("Model"), "context_window": len("Context Window"), "last_scanned": len("Last Scanned")} + + # Feature column widths + for feature in feature_mapping.keys(): + widths[feature] = len(feature) + + # Check all model data for maximum widths + for provider_data in all_provider_data.values(): + for model_info in provider_data: + # Model name width (including backticks) + model_width = len(f"`{model_info['name']}`") + widths["model"] = max(widths["model"], model_width) + + # Context window width (with commas) + context_width = len(f"{model_info['context_window']:,}") + widths["context_window"] = max(widths["context_window"], context_width) + + # Last scanned width + widths["last_scanned"] = max(widths["last_scanned"], len(str(model_info["last_scanned"]))) + + # Feature support symbols are always 2 chars, so no need to check + + return widths + + +def process_model_sweep_report(input_file, output_file, config_file=None, debug=False): + """Convert model sweep JSON data to MDX report.""" + + # Load feature mappings from config file + feature_mapping = load_feature_mappings(config_file) + + # if debug: + # print("DEBUG: Feature mappings loaded:") + # for feature, tests in feature_mapping.items(): + # print(f" {feature}: {tests}") + # print() + + # Read the JSON data + with open(input_file, "r") as f: + data = json.load(f) + + tests = data.get("tests", []) + + # if debug: + # print("DEBUG: Tests loaded:") + # print([test['outcome'] for test in tests if 'haiku' in test['nodeid']]) + + # Calculate summary statistics + providers = set(test["metadata"]["llm_config"]["provider_name"] for test in tests) + models = set(test["metadata"]["llm_config"]["model"] for test in tests) + total_tests = len(tests) + + # Start building the MDX + mdx_lines = [ + "---", + "title: Support Models", + f"generated: {datetime.now().isoformat()}", + "---", + "", + "# Supported Models", + "", + "## Overview", + "", + "Letta routinely runs automated scans against available providers and models. These are the results of the latest scan.", + "", + f"Ran {total_tests} tests against {len(models)} models across {len(providers)} providers on {datetime.now().strftime('%B %dth, %Y')}", + "", + "", + ] + + # Group tests by provider + provider_groups = defaultdict(list) + for test in tests: + provider_name = test["metadata"]["llm_config"]["provider_name"] + provider_groups[provider_name].append(test) + + # Process all providers first to collect model data + all_provider_data = {} + provider_support_scores = {} + + for provider_name in provider_groups.keys(): + provider_tests = provider_groups[provider_name] + + # Group tests by model within this provider + model_groups = defaultdict(list) + for test in provider_tests: + model_name = test["metadata"]["llm_config"]["model"] + model_groups[model_name].append(test) + + # Process all models to calculate support scores for ranking + model_data = [] + for model_name in model_groups.keys(): + model_tests = model_groups[model_name] + + # if debug: + # print(f"DEBUG: Processing model '{model_name}' in provider '{provider_name}'") + + # Extract unique test names for passed and failed tests + passed_tests = set() + failed_tests = set() + all_test_names = set() + + for test in model_tests: + # Extract test name from nodeid (split on :: and [) + test_name = test["nodeid"].split("::")[1].split("[")[0] + all_test_names.add(test_name) + + # if debug: + # print(f" Test name: {test_name}") + # print(f" Outcome: {test}") + if test["outcome"] == "passed": + passed_tests.add(test_name) + elif test["outcome"] == "failed": + failed_tests.add(test_name) + + # if debug: + # print(f" All test names found: {sorted(all_test_names)}") + # print(f" Passed tests: {sorted(passed_tests)}") + # print(f" Failed tests: {sorted(failed_tests)}") + + # Categorize tests into features + categorized_tests = categorize_tests(all_test_names, feature_mapping) + + # if debug: + # print(f" Categorized tests:") + # for feature, tests in categorized_tests.items(): + # print(f" {feature}: {tests}") + + # Determine support status for each feature + feature_support = {} + for feature_name in feature_mapping.keys(): + feature_support[feature_name] = get_support_status(passed_tests, categorized_tests[feature_name]) + + # if debug: + # print(f" Feature support:") + # for feature, status in feature_support.items(): + # print(f" {feature}: {status}") + # print() + + # Get context window and last scanned time + context_window = model_tests[0]["metadata"]["llm_config"]["context_window"] + + # Try to get time_last_scanned from metadata, fallback to current time + try: + last_scanned = model_tests[0]["metadata"].get( + "time_last_scanned", model_tests[0]["metadata"].get("timestamp", datetime.now().isoformat()) + ) + # Format timestamp if it's a full ISO string + if "T" in str(last_scanned): + last_scanned = str(last_scanned).split("T")[0] # Just the date part + except Exception: + last_scanned = "Unknown" + + # Calculate support score for ranking + feature_order = list(feature_mapping.keys()) + support_score = calculate_support_score(feature_support, feature_order) + + # Store model data for sorting + model_data.append( + { + "name": model_name, + "feature_support": feature_support, + "context_window": context_window, + "last_scanned": last_scanned, + "support_score": support_score, + "failed_tests": failed_tests, + "passed_tests": passed_tests, + "categorized_tests": categorized_tests, + } + ) + + # Sort models by support score (descending) then by name (ascending) + model_data.sort(key=lambda x: (-x["support_score"], x["name"])) + + # Store provider data + all_provider_data[provider_name] = model_data + provider_support_scores[provider_name] = calculate_provider_support_score(model_data, list(feature_mapping.keys())) + + # Calculate column widths for consistent formatting (add details column) + column_widths = calculate_column_widths(all_provider_data, feature_mapping) + column_widths["details"] = len("Details") + + # Sort providers by support score (descending) then by name (ascending) + sorted_providers = sorted(provider_support_scores.keys(), key=lambda x: (-provider_support_scores[x], x)) + + # Generate tables for all providers first + for provider_name in sorted_providers: + model_data = all_provider_data[provider_name] + support_score = provider_support_scores[provider_name] + + # Create dynamic headers with proper padding and centering + feature_names = list(feature_mapping.keys()) + + # Build header row with left-aligned first column, centered others + header_parts = [f"{'Model':<{column_widths['model']}}"] + for feature in feature_names: + header_parts.append(f"{feature:^{column_widths[feature]}}") + header_parts.extend( + [ + f"{'Context Window':^{column_widths['context_window']}}", + f"{'Last Scanned':^{column_widths['last_scanned']}}", + f"{'Details':^{column_widths['details']}}", + ] + ) + header_row = "| " + " | ".join(header_parts) + " |" + + # Build separator row with left-aligned first column, centered others + separator_parts = [f"{'-' * column_widths['model']}"] + for feature in feature_names: + separator_parts.append(f":{'-' * (column_widths[feature] - 2)}:") + separator_parts.extend( + [ + f":{'-' * (column_widths['context_window'] - 2)}:", + f":{'-' * (column_widths['last_scanned'] - 2)}:", + f":{'-' * (column_widths['details'] - 2)}:", + ] + ) + separator_row = "|" + "|".join(separator_parts) + "|" + + # Add provider section without percentage + mdx_lines.extend([f"## {provider_name}", "", header_row, separator_row]) + + # Generate table rows for sorted models with proper padding + for model_info in model_data: + # Create anchor for model details + model_anchor = model_info["name"].replace("/", "_").replace(":", "_").replace("-", "_").lower() + details_anchor = f"{provider_name.lower().replace(' ', '_')}_{model_anchor}_details" + + # Build row with left-aligned first column, centered others + row_parts = [f"`{model_info['name']}`".ljust(column_widths["model"])] + for feature in feature_names: + row_parts.append(f"{model_info['feature_support'][feature]:^{column_widths[feature]}}") + row_parts.extend( + [ + f"{model_info['context_window']:,}".center(column_widths["context_window"]), + f"{model_info['last_scanned']}".center(column_widths["last_scanned"]), + f"[View](#{details_anchor})".center(column_widths["details"]), + ] + ) + row = "| " + " | ".join(row_parts) + " |" + mdx_lines.append(row) + + # Add spacing between provider tables + mdx_lines.extend(["", ""]) + + # Add detailed test results section after all tables + mdx_lines.extend(["---", "", "# Detailed Test Results", ""]) + + for provider_name in sorted_providers: + model_data = all_provider_data[provider_name] + mdx_lines.extend([f"## {provider_name}", ""]) + + for model_info in model_data: + model_anchor = model_info["name"].replace("/", "_").replace(":", "_").replace("-", "_").lower() + details_anchor = f"{provider_name.lower().replace(' ', '_')}_{model_anchor}_details" + mdx_lines.append(f'') + mdx_lines.append(f"### {model_info['name']}") + mdx_lines.append("") + + # Add test details + test_details = generate_test_details(model_info, feature_mapping) + mdx_lines.extend(test_details) + + # Add spacing between providers in details section + mdx_lines.extend(["", ""]) + + # Write the MDX file + with open(output_file, "w") as f: + f.write("\n".join(mdx_lines)) + + print(f"Model sweep report saved to {output_file}") + + +def main(): + input_file = "model_sweep_report.json" + output_file = "model_sweep_report.mdx" + config_file = None + debug = False + + # Allow command line arguments + if len(sys.argv) > 1: + # Use the file located in the same directory as this script + script_dir = os.path.dirname(os.path.abspath(__file__)) + input_file = os.path.join(script_dir, sys.argv[1]) + if len(sys.argv) > 2: + # Use the file located in the same directory as this script + script_dir = os.path.dirname(os.path.abspath(__file__)) + output_file = os.path.join(script_dir, sys.argv[2]) + if len(sys.argv) > 3: + config_file = sys.argv[3] + if len(sys.argv) > 4 and sys.argv[4] == "--debug": + debug = True + + try: + process_model_sweep_report(input_file, output_file, config_file, debug) + except FileNotFoundError: + print(f"Error: Could not find input file '{input_file}'") + sys.exit(1) + except json.JSONDecodeError: + print(f"Error: Invalid JSON in file '{input_file}'") + sys.exit(1) + except Exception as e: + print(f"Error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/model-sweep/model_sweep.py b/.github/scripts/model-sweep/model_sweep.py new file mode 100644 index 0000000..086ea0a --- /dev/null +++ b/.github/scripts/model-sweep/model_sweep.py @@ -0,0 +1,783 @@ +import base64 +import json +import os +import time +import uuid +from typing import Any, Dict, List + +import httpx +import pytest +from letta_client import Letta, MessageCreate, Run +from letta_client.core.api_error import ApiError +from letta_client.types import ( + AssistantMessage, + Base64Image, + ImageContent, + LettaUsageStatistics, + ReasoningMessage, + TextContent, + ToolCallMessage, + ToolReturnMessage, + UrlImage, + UserMessage, +) + +from letta.schemas.agent import AgentState +from letta.schemas.llm_config import LLMConfig + +# ------------------------------ +# Helper Functions and Constants +# ------------------------------ + + +def get_llm_config(filename: str, llm_config_dir: str = "tests/configs/llm_model_configs") -> LLMConfig: + filename = os.path.join(llm_config_dir, filename) + with open(filename, "r") as f: + config_data = json.load(f) + llm_config = LLMConfig(**config_data) + return llm_config + + +def roll_dice(num_sides: int) -> int: + """ + Returns a random number between 1 and num_sides. + Args: + num_sides (int): The number of sides on the die. + Returns: + int: A random integer between 1 and num_sides, representing the die roll. + """ + import random + + return random.randint(1, num_sides) + + +USER_MESSAGE_OTID = str(uuid.uuid4()) +USER_MESSAGE_RESPONSE: str = "Teamwork makes the dream work" +USER_MESSAGE_FORCE_REPLY: List[MessageCreate] = [ + MessageCreate( + role="user", + content=f"This is an automated test message. Call the send_message tool with the message '{USER_MESSAGE_RESPONSE}'.", + otid=USER_MESSAGE_OTID, + ) +] +USER_MESSAGE_ROLL_DICE: List[MessageCreate] = [ + MessageCreate( + role="user", + content="This is an automated test message. Call the roll_dice tool with 16 sides and tell me the outcome.", + otid=USER_MESSAGE_OTID, + ) +] +URL_IMAGE = "https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg" +USER_MESSAGE_URL_IMAGE: List[MessageCreate] = [ + MessageCreate( + role="user", + content=[ + ImageContent(source=UrlImage(url=URL_IMAGE)), + TextContent(text="What is in this image?"), + ], + otid=USER_MESSAGE_OTID, + ) +] +BASE64_IMAGE = base64.standard_b64encode(httpx.get(URL_IMAGE).content).decode("utf-8") +USER_MESSAGE_BASE64_IMAGE: List[MessageCreate] = [ + MessageCreate( + role="user", + content=[ + ImageContent(source=Base64Image(data=BASE64_IMAGE, media_type="image/jpeg")), + TextContent(text="What is in this image?"), + ], + otid=USER_MESSAGE_OTID, + ) +] +all_configs = [ + "openai-gpt-4o-mini.json", + # "azure-gpt-4o-mini.json", # TODO: Re-enable on new agent loop + "claude-3-5-sonnet.json", + "claude-4-sonnet-extended.json", + "claude-3-7-sonnet-extended.json", + "gemini-1.5-pro.json", + "gemini-2.5-flash-vertex.json", + "gemini-2.5-pro-vertex.json", + "together-qwen-2.5-72b-instruct.json", + "ollama.json", +] +requested = os.getenv("LLM_CONFIG_FILE") +filenames = [requested] if requested else all_configs +TESTED_LLM_CONFIGS: List[LLMConfig] = [get_llm_config(fn) for fn in filenames] + + +def assert_greeting_with_assistant_message_response( + messages: List[Any], + streaming: bool = False, + token_streaming: bool = False, + from_db: bool = False, +) -> None: + """ + Asserts that the messages list follows the expected sequence: + ReasoningMessage -> AssistantMessage. + """ + expected_message_count = 3 if streaming or from_db else 2 + assert len(messages) == expected_message_count + + index = 0 + if from_db: + assert isinstance(messages[index], UserMessage) + assert messages[index].otid == USER_MESSAGE_OTID + index += 1 + + # Agent Step 1 + assert isinstance(messages[index], ReasoningMessage) + assert messages[index].otid and messages[index].otid[-1] == "0" + index += 1 + + assert isinstance(messages[index], AssistantMessage) + if not token_streaming: + assert USER_MESSAGE_RESPONSE in messages[index].content + assert messages[index].otid and messages[index].otid[-1] == "1" + index += 1 + + if streaming: + assert isinstance(messages[index], LettaUsageStatistics) + assert messages[index].prompt_tokens > 0 + assert messages[index].completion_tokens > 0 + assert messages[index].total_tokens > 0 + assert messages[index].step_count > 0 + + +def assert_greeting_without_assistant_message_response( + messages: List[Any], + streaming: bool = False, + token_streaming: bool = False, + from_db: bool = False, +) -> None: + """ + Asserts that the messages list follows the expected sequence: + ReasoningMessage -> ToolCallMessage -> ToolReturnMessage. + """ + expected_message_count = 4 if streaming or from_db else 3 + assert len(messages) == expected_message_count + + index = 0 + if from_db: + assert isinstance(messages[index], UserMessage) + assert messages[index].otid == USER_MESSAGE_OTID + index += 1 + + # Agent Step 1 + assert isinstance(messages[index], ReasoningMessage) + assert messages[index].otid and messages[index].otid[-1] == "0" + index += 1 + + assert isinstance(messages[index], ToolCallMessage) + assert messages[index].tool_call.name == "send_message" + if not token_streaming: + assert USER_MESSAGE_RESPONSE in messages[index].tool_call.arguments + assert messages[index].otid and messages[index].otid[-1] == "1" + index += 1 + + # Agent Step 2 + assert isinstance(messages[index], ToolReturnMessage) + assert messages[index].otid and messages[index].otid[-1] == "0" + index += 1 + + if streaming: + assert isinstance(messages[index], LettaUsageStatistics) + + +def assert_tool_call_response( + messages: List[Any], + streaming: bool = False, + from_db: bool = False, +) -> None: + """ + Asserts that the messages list follows the expected sequence: + ReasoningMessage -> ToolCallMessage -> ToolReturnMessage -> + ReasoningMessage -> AssistantMessage. + """ + expected_message_count = 6 if streaming else 7 if from_db else 5 + assert len(messages) == expected_message_count + + index = 0 + if from_db: + assert isinstance(messages[index], UserMessage) + assert messages[index].otid == USER_MESSAGE_OTID + index += 1 + + # Agent Step 1 + assert isinstance(messages[index], ReasoningMessage) + assert messages[index].otid and messages[index].otid[-1] == "0" + index += 1 + + assert isinstance(messages[index], ToolCallMessage) + assert messages[index].otid and messages[index].otid[-1] == "1" + index += 1 + + # Agent Step 2 + assert isinstance(messages[index], ToolReturnMessage) + assert messages[index].otid and messages[index].otid[-1] == "0" + index += 1 + + # Hidden User Message + if from_db: + assert isinstance(messages[index], UserMessage) + assert "request_heartbeat=true" in messages[index].content + index += 1 + + # Agent Step 3 + assert isinstance(messages[index], ReasoningMessage) + assert messages[index].otid and messages[index].otid[-1] == "0" + index += 1 + + assert isinstance(messages[index], AssistantMessage) + assert messages[index].otid and messages[index].otid[-1] == "1" + index += 1 + + if streaming: + assert isinstance(messages[index], LettaUsageStatistics) + + +def assert_image_input_response( + messages: List[Any], + streaming: bool = False, + token_streaming: bool = False, + from_db: bool = False, +) -> None: + """ + Asserts that the messages list follows the expected sequence: + ReasoningMessage -> AssistantMessage. + """ + expected_message_count = 3 if streaming or from_db else 2 + assert len(messages) == expected_message_count + + index = 0 + if from_db: + assert isinstance(messages[index], UserMessage) + assert messages[index].otid == USER_MESSAGE_OTID + index += 1 + + # Agent Step 1 + assert isinstance(messages[index], ReasoningMessage) + assert messages[index].otid and messages[index].otid[-1] == "0" + index += 1 + + assert isinstance(messages[index], AssistantMessage) + assert messages[index].otid and messages[index].otid[-1] == "1" + index += 1 + + if streaming: + assert isinstance(messages[index], LettaUsageStatistics) + assert messages[index].prompt_tokens > 0 + assert messages[index].completion_tokens > 0 + assert messages[index].total_tokens > 0 + assert messages[index].step_count > 0 + + +def accumulate_chunks(chunks: List[Any]) -> List[Any]: + """ + Accumulates chunks into a list of messages. + """ + messages = [] + current_message = None + prev_message_type = None + for chunk in chunks: + current_message_type = chunk.message_type + if prev_message_type != current_message_type: + messages.append(current_message) + current_message = None + if current_message is None: + current_message = chunk + else: + pass # TODO: actually accumulate the chunks. For now we only care about the count + prev_message_type = current_message_type + messages.append(current_message) + return [m for m in messages if m is not None] + + +def wait_for_run_completion(client: Letta, run_id: str, timeout: float = 30.0, interval: float = 0.5) -> Run: + start = time.time() + while True: + run = client.runs.retrieve(run_id) + if run.status == "completed": + return run + if run.status == "failed": + raise RuntimeError(f"Run {run_id} did not complete: status = {run.status}") + if time.time() - start > timeout: + raise TimeoutError(f"Run {run_id} did not complete within {timeout} seconds (last status: {run.status})") + time.sleep(interval) + + +def assert_tool_response_dict_messages(messages: List[Dict[str, Any]]) -> None: + """ + Asserts that a list of message dictionaries contains the expected types and statuses. + + Expected order: + 1. reasoning_message + 2. tool_call_message + 3. tool_return_message (with status 'success') + 4. reasoning_message + 5. assistant_message + """ + assert isinstance(messages, list) + assert messages[0]["message_type"] == "reasoning_message" + assert messages[1]["message_type"] == "assistant_message" + + +# ------------------------------ +# Test Cases +# ------------------------------ + +# def test_that_ci_workflow_works( +# disable_e2b_api_key: Any, +# client: Letta, +# agent_state: AgentState, +# llm_config: LLMConfig, +# json_metadata: pytest.FixtureRequest, +# ) -> None: +# """ +# Tests that the CI workflow works. +# """ +# json_metadata["test_type"] = "debug" + + +def test_greeting_with_assistant_message( + disable_e2b_api_key: Any, + client: Letta, + agent_state: AgentState, + llm_config: LLMConfig, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a message with a synchronous client. + Verifies that the response messages follow the expected order. + """ + json_metadata["llm_config"] = dict(llm_config) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) + response = client.agents.messages.create( + agent_id=agent_state.id, + messages=USER_MESSAGE_FORCE_REPLY, + ) + assert_greeting_with_assistant_message_response(response.messages) + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id) + assert_greeting_with_assistant_message_response(messages_from_db, from_db=True) + + +def test_greeting_without_assistant_message( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a message with a synchronous client. + Verifies that the response messages follow the expected order. + """ + json_metadata["llm_config"] = dict(llm_config) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) + response = client.agents.messages.create( + agent_id=agent_state.id, + messages=USER_MESSAGE_FORCE_REPLY, + use_assistant_message=False, + ) + assert_greeting_without_assistant_message_response(response.messages) + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id, use_assistant_message=False) + assert_greeting_without_assistant_message_response(messages_from_db, from_db=True) + + +def test_tool_call( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a message with a synchronous client. + Verifies that the response messages follow the expected order. + """ + json_metadata["llm_config"] = dict(llm_config) + dice_tool = client.tools.upsert_from_function(func=roll_dice) + client.agents.tools.attach(agent_id=agent_state.id, tool_id=dice_tool.id) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) + response = client.agents.messages.create( + agent_id=agent_state.id, + messages=USER_MESSAGE_ROLL_DICE, + ) + assert_tool_call_response(response.messages) + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id) + assert_tool_call_response(messages_from_db, from_db=True) + + +def test_url_image_input( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a message with a synchronous client. + Verifies that the response messages follow the expected order. + """ + json_metadata["llm_config"] = dict(llm_config) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) + response = client.agents.messages.create( + agent_id=agent_state.id, + messages=USER_MESSAGE_URL_IMAGE, + ) + assert_image_input_response(response.messages) + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id) + assert_image_input_response(messages_from_db, from_db=True) + + +def test_base64_image_input( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a message with a synchronous client. + Verifies that the response messages follow the expected order. + """ + json_metadata["llm_config"] = dict(llm_config) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) + response = client.agents.messages.create( + agent_id=agent_state.id, + messages=USER_MESSAGE_BASE64_IMAGE, + ) + assert_image_input_response(response.messages) + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id) + assert_image_input_response(messages_from_db, from_db=True) + + +def test_agent_loop_error( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a message with a synchronous client. + Verifies that no new messages are persisted on error. + """ + json_metadata["llm_config"] = dict(llm_config) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + tools = agent_state.tools + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config, tool_ids=[]) + with pytest.raises(ApiError): + client.agents.messages.create( + agent_id=agent_state.id, + messages=USER_MESSAGE_FORCE_REPLY, + ) + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id) + assert len(messages_from_db) == 0 + client.agents.modify(agent_id=agent_state.id, tool_ids=[t.id for t in tools]) + + +def test_step_streaming_greeting_with_assistant_message( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a streaming message with a synchronous client. + Checks that each chunk in the stream has the correct message types. + """ + json_metadata["llm_config"] = dict(llm_config) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) + response = client.agents.messages.create_stream( + agent_id=agent_state.id, + messages=USER_MESSAGE_FORCE_REPLY, + ) + chunks = list(response) + messages = accumulate_chunks(chunks) + assert_greeting_with_assistant_message_response(messages, streaming=True) + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id) + assert_greeting_with_assistant_message_response(messages_from_db, from_db=True) + + +def test_step_streaming_greeting_without_assistant_message( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a streaming message with a synchronous client. + Checks that each chunk in the stream has the correct message types. + """ + json_metadata["llm_config"] = dict(llm_config) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) + response = client.agents.messages.create_stream( + agent_id=agent_state.id, + messages=USER_MESSAGE_FORCE_REPLY, + use_assistant_message=False, + ) + chunks = list(response) + messages = accumulate_chunks(chunks) + assert_greeting_without_assistant_message_response(messages, streaming=True) + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id, use_assistant_message=False) + assert_greeting_without_assistant_message_response(messages_from_db, from_db=True) + + +def test_step_streaming_tool_call( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a streaming message with a synchronous client. + Checks that each chunk in the stream has the correct message types. + """ + json_metadata["llm_config"] = dict(llm_config) + dice_tool = client.tools.upsert_from_function(func=roll_dice) + agent_state = client.agents.tools.attach(agent_id=agent_state.id, tool_id=dice_tool.id) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) + response = client.agents.messages.create_stream( + agent_id=agent_state.id, + messages=USER_MESSAGE_ROLL_DICE, + ) + chunks = list(response) + messages = accumulate_chunks(chunks) + assert_tool_call_response(messages, streaming=True) + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id) + assert_tool_call_response(messages_from_db, from_db=True) + + +def test_step_stream_agent_loop_error( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a message with a synchronous client. + Verifies that no new messages are persisted on error. + """ + json_metadata["llm_config"] = dict(llm_config) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + tools = agent_state.tools + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config, tool_ids=[]) + with pytest.raises(ApiError): + response = client.agents.messages.create_stream( + agent_id=agent_state.id, + messages=USER_MESSAGE_FORCE_REPLY, + ) + list(response) + + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id) + assert len(messages_from_db) == 0 + client.agents.modify(agent_id=agent_state.id, tool_ids=[t.id for t in tools]) + + +def test_token_streaming_greeting_with_assistant_message( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a streaming message with a synchronous client. + Checks that each chunk in the stream has the correct message types. + """ + json_metadata["llm_config"] = dict(llm_config) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) + response = client.agents.messages.create_stream( + agent_id=agent_state.id, + messages=USER_MESSAGE_FORCE_REPLY, + stream_tokens=True, + ) + chunks = list(response) + messages = accumulate_chunks(chunks) + assert_greeting_with_assistant_message_response(messages, streaming=True, token_streaming=True) + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id) + assert_greeting_with_assistant_message_response(messages_from_db, from_db=True) + + +def test_token_streaming_greeting_without_assistant_message( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a streaming message with a synchronous client. + Checks that each chunk in the stream has the correct message types. + """ + json_metadata["llm_config"] = dict(llm_config) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) + response = client.agents.messages.create_stream( + agent_id=agent_state.id, + messages=USER_MESSAGE_FORCE_REPLY, + use_assistant_message=False, + stream_tokens=True, + ) + chunks = list(response) + messages = accumulate_chunks(chunks) + assert_greeting_without_assistant_message_response(messages, streaming=True, token_streaming=True) + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id, use_assistant_message=False) + assert_greeting_without_assistant_message_response(messages_from_db, from_db=True) + + +def test_token_streaming_tool_call( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a streaming message with a synchronous client. + Checks that each chunk in the stream has the correct message types. + """ + json_metadata["llm_config"] = dict(llm_config) + dice_tool = client.tools.upsert_from_function(func=roll_dice) + agent_state = client.agents.tools.attach(agent_id=agent_state.id, tool_id=dice_tool.id) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) + response = client.agents.messages.create_stream( + agent_id=agent_state.id, + messages=USER_MESSAGE_ROLL_DICE, + stream_tokens=True, + ) + chunks = list(response) + messages = accumulate_chunks(chunks) + assert_tool_call_response(messages, streaming=True) + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id) + assert_tool_call_response(messages_from_db, from_db=True) + + +def test_token_streaming_agent_loop_error( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a message with a synchronous client. + Verifies that no new messages are persisted on error. + """ + json_metadata["llm_config"] = dict(llm_config) + last_message = client.agents.messages.list(agent_id=agent_state.id, limit=1) + tools = agent_state.tools + agent_state = client.agents.modify(agent_id=agent_state.id, llm_config=llm_config, tool_ids=[]) + try: + response = client.agents.messages.create_stream( + agent_id=agent_state.id, + messages=USER_MESSAGE_FORCE_REPLY, + stream_tokens=True, + ) + list(response) + except Exception: + pass # only some models throw an error TODO: make this consistent + + messages_from_db = client.agents.messages.list(agent_id=agent_state.id, after=last_message[0].id) + assert len(messages_from_db) == 0 + client.agents.modify(agent_id=agent_state.id, tool_ids=[t.id for t in tools]) + + +def test_async_greeting_with_assistant_message( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + agent_state: AgentState, + json_metadata: pytest.FixtureRequest, +) -> None: + """ + Tests sending a message as an asynchronous job using the synchronous client. + Waits for job completion and asserts that the result messages are as expected. + """ + json_metadata["llm_config"] = dict(llm_config) + client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) + + run = client.agents.messages.create_async( + agent_id=agent_state.id, + messages=USER_MESSAGE_FORCE_REPLY, + ) + run = wait_for_run_completion(client, run.id) + + result = run.metadata.get("result") + assert result is not None, "Run metadata missing 'result' key" + + messages = result["messages"] + assert_tool_response_dict_messages(messages) + + +def test_auto_summarize( + disable_e2b_api_key: Any, + client: Letta, + llm_config: LLMConfig, + json_metadata: pytest.FixtureRequest, +) -> None: + """Test that summarization is automatically triggered.""" + json_metadata["llm_config"] = dict(llm_config) + + # pydantic prevents us for overriding the context window paramter in the passed LLMConfig + new_llm_config = llm_config.model_dump() + new_llm_config["context_window"] = 3000 + pinned_context_window_llm_config = LLMConfig(**new_llm_config) + + send_message_tool = client.tools.list(name="send_message")[0] + temp_agent_state = client.agents.create( + include_base_tools=False, + tool_ids=[send_message_tool.id], + llm_config=pinned_context_window_llm_config, + embedding="letta/letta-free", + tags=["supervisor"], + ) + + philosophical_question = """ +You know, sometimes I wonder if the entire structure of our lives is built on a series of unexamined assumptions we just silently agreed to somewhere along the way—like how we all just decided that five days a week of work and two days of “rest” constitutes balance, or how 9-to-5 became the default rhythm of a meaningful life, or even how the idea of “success” got boiled down to job titles and property ownership and productivity metrics on a LinkedIn profile, when maybe none of that is actually what makes a life feel full, or grounded, or real. And then there’s the weird paradox of ambition, how we're taught to chase it like a finish line that keeps moving, constantly redefining itself right as you’re about to grasp it—because even when you get the job, or the degree, or the validation, there's always something next, something more, like a treadmill with invisible settings you didn’t realize were turned up all the way. + +And have you noticed how we rarely stop to ask who set those definitions for us? Like was there ever a council that decided, yes, owning a home by thirty-five and retiring by sixty-five is the universal template for fulfillment? Or did it just accumulate like cultural sediment over generations, layered into us so deeply that questioning it feels uncomfortable, even dangerous? And isn’t it strange that we spend so much of our lives trying to optimize things—our workflows, our diets, our sleep, our morning routines—as though the point of life is to operate more efficiently rather than to experience it more richly? We build these intricate systems, these rulebooks for being a “high-functioning” human, but where in all of that is the space for feeling lost, for being soft, for wandering without a purpose just because it’s a sunny day and your heart is tugging you toward nowhere in particular? + +Sometimes I lie awake at night and wonder if all the noise we wrap around ourselves—notifications, updates, performance reviews, even our internal monologues—might be crowding out the questions we were meant to live into slowly, like how to love better, or how to forgive ourselves, or what the hell we’re even doing here in the first place. And when you strip it all down—no goals, no KPIs, no curated identity—what’s actually left of us? Are we just a sum of the roles we perform, or is there something quieter underneath that we've forgotten how to hear? + +And if there is something underneath all of it—something real, something worth listening to—then how do we begin to uncover it, gently, without rushing or reducing it to another task on our to-do list? + """ + + MAX_ATTEMPTS = 10 + prev_length = None + + for attempt in range(MAX_ATTEMPTS): + client.agents.messages.create( + agent_id=temp_agent_state.id, + messages=[MessageCreate(role="user", content=philosophical_question)], + ) + + temp_agent_state = client.agents.retrieve(agent_id=temp_agent_state.id) + message_ids = temp_agent_state.message_ids + current_length = len(message_ids) + + print("LENGTH OF IN_CONTEXT_MESSAGES:", current_length) + + if prev_length is not None and current_length <= prev_length: + # TODO: Add more stringent checks here + print(f"Summarization was triggered, detected current_length {current_length} is at least prev_length {prev_length}.") + break + + prev_length = current_length + else: + raise AssertionError("Summarization was not triggered after 10 messages") diff --git a/.github/scripts/model-sweep/supported-models.mdx b/.github/scripts/model-sweep/supported-models.mdx new file mode 100644 index 0000000..a8a203c --- /dev/null +++ b/.github/scripts/model-sweep/supported-models.mdx @@ -0,0 +1,4551 @@ +--- +title: Support Models +generated: 2025-06-20T16:40:44.072054 +--- + +# Supported Models + +## Overview + +Letta routinely runs automated scans against available providers and models. These are the results of the latest scan. + +Ran 2464 tests against 154 models across 7 providers on June 20th, 2025 + + +## anthropic + +| Model | Basic | Token Streaming | Multimodal | Context Window | Last Scanned | Details | +|---------------------------------------------------|:---:|:-------------:|:--------:|:------------:|:----------:|:-----:| +| `claude-3-5-haiku-20241022` | ✅ | ✅ | ✅ | 200,000 | 2025-06-20 | [View](#anthropic_claude_3_5_haiku_20241022_details) | +| `claude-3-5-sonnet-20241022` | ✅ | ✅ | ✅ | 200,000 | 2025-06-20 | [View](#anthropic_claude_3_5_sonnet_20241022_details) | +| `claude-3-7-sonnet-20250219` | ✅ | ✅ | ✅ | 200,000 | 2025-06-20 | [View](#anthropic_claude_3_7_sonnet_20250219_details) | +| `claude-sonnet-4-20250514` | ✅ | ✅ | ✅ | 200,000 | 2025-06-20 | [View](#anthropic_claude_sonnet_4_20250514_details) | +| `claude-opus-4-20250514` | ✅ | ✅ | ⚠️ | 200,000 | 2025-06-20 | [View](#anthropic_claude_opus_4_20250514_details) | +| `claude-3-5-sonnet-20240620` | ⚠️ | ❌ | ✅ | 200,000 | 2025-06-20 | [View](#anthropic_claude_3_5_sonnet_20240620_details) | +| `claude-3-haiku-20240307` | ⚠️ | ❌ | ✅ | 200,000 | 2025-06-20 | [View](#anthropic_claude_3_haiku_20240307_details) | +| `claude-3-opus-20240229` | ⚠️ | ❌ | ✅ | 200,000 | 2025-06-20 | [View](#anthropic_claude_3_opus_20240229_details) | +| `claude-3-sonnet-20240229` | ❌ | ❌ | ❌ | 200,000 | 2025-06-20 | [View](#anthropic_claude_3_sonnet_20240229_details) | + + +## openai + +| Model | Basic | Token Streaming | Multimodal | Context Window | Last Scanned | Details | +|---------------------------------------------------|:---:|:-------------:|:--------:|:------------:|:----------:|:-----:| +| `gpt-4.1` | ✅ | ✅ | ✅ | 1,047,576 | 2025-06-20 | [View](#openai_gpt_4.1_details) | +| `gpt-4.1-2025-04-14` | ✅ | ✅ | ✅ | 1,047,576 | 2025-06-20 | [View](#openai_gpt_4.1_2025_04_14_details) | +| `gpt-4.1-nano-2025-04-14` | ✅ | ✅ | ✅ | 1,047,576 | 2025-06-20 | [View](#openai_gpt_4.1_nano_2025_04_14_details) | +| `gpt-4o` | ✅ | ✅ | ✅ | 128,000 | 2025-06-20 | [View](#openai_gpt_4o_details) | +| `gpt-4o-2024-05-13` | ✅ | ✅ | ✅ | 128,000 | 2025-06-20 | [View](#openai_gpt_4o_2024_05_13_details) | +| `gpt-4-turbo` | ✅ | ✅ | ⚠️ | 8,192 | 2025-06-20 | [View](#openai_gpt_4_turbo_details) | +| `gpt-4.1-mini` | ✅ | ✅ | ⚠️ | 1,047,576 | 2025-06-20 | [View](#openai_gpt_4.1_mini_details) | +| `gpt-4.5-preview` | ✅ | ✅ | ⚠️ | 128,000 | 2025-06-20 | [View](#openai_gpt_4.5_preview_details) | +| `gpt-4.5-preview-2025-02-27` | ✅ | ✅ | ⚠️ | 128,000 | 2025-06-20 | [View](#openai_gpt_4.5_preview_2025_02_27_details) | +| `gpt-4o-2024-08-06` | ✅ | ✅ | ⚠️ | 128,000 | 2025-06-20 | [View](#openai_gpt_4o_2024_08_06_details) | +| `gpt-4-0613` | ✅ | ✅ | ❌ | 8,192 | 2025-06-20 | [View](#openai_gpt_4_0613_details) | +| `gpt-4-1106-preview` | ✅ | ✅ | ❌ | 128,000 | 2025-06-20 | [View](#openai_gpt_4_1106_preview_details) | +| `gpt-4-turbo-2024-04-09` | ✅ | ⚠️ | ✅ | 128,000 | 2025-06-20 | [View](#openai_gpt_4_turbo_2024_04_09_details) | +| `gpt-4.1-mini-2025-04-14` | ⚠️ | ✅ | ✅ | 1,047,576 | 2025-06-20 | [View](#openai_gpt_4.1_mini_2025_04_14_details) | +| `gpt-4.1-nano` | ⚠️ | ✅ | ✅ | 1,047,576 | 2025-06-20 | [View](#openai_gpt_4.1_nano_details) | +| `gpt-4o-2024-11-20` | ⚠️ | ✅ | ✅ | 8,192 | 2025-06-20 | [View](#openai_gpt_4o_2024_11_20_details) | +| `gpt-4-turbo-preview` | ✅ | ⚠️ | ❌ | 128,000 | 2025-06-20 | [View](#openai_gpt_4_turbo_preview_details) | +| `gpt-4-0125-preview` | ⚠️ | ✅ | ❌ | 128,000 | 2025-06-20 | [View](#openai_gpt_4_0125_preview_details) | +| `gpt-4o-mini` | ⚠️ | ⚠️ | ⚠️ | 128,000 | 2025-06-20 | [View](#openai_gpt_4o_mini_details) | +| `gpt-4o-mini-2024-07-18` | ⚠️ | ⚠️ | ❌ | 128,000 | 2025-06-20 | [View](#openai_gpt_4o_mini_2024_07_18_details) | +| `gpt-4` | ⚠️ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#openai_gpt_4_details) | +| `o1` | ⚠️ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#openai_o1_details) | +| `o1-2024-12-17` | ⚠️ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#openai_o1_2024_12_17_details) | +| `o3` | ⚠️ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#openai_o3_details) | +| `o3-2025-04-16` | ⚠️ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#openai_o3_2025_04_16_details) | +| `o3-mini` | ⚠️ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#openai_o3_mini_details) | +| `o3-mini-2025-01-31` | ⚠️ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#openai_o3_mini_2025_01_31_details) | +| `o3-pro` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#openai_o3_pro_details) | +| `o3-pro-2025-06-10` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#openai_o3_pro_2025_06_10_details) | + + +## google_ai + +| Model | Basic | Token Streaming | Multimodal | Context Window | Last Scanned | Details | +|---------------------------------------------------|:---:|:-------------:|:--------:|:------------:|:----------:|:-----:| +| `gemini-1.5-pro` | ✅ | ✅ | ✅ | 2,000,000 | 2025-06-20 | [View](#google_ai_gemini_1.5_pro_details) | +| `gemini-1.5-pro-002` | ✅ | ✅ | ✅ | 2,000,000 | 2025-06-20 | [View](#google_ai_gemini_1.5_pro_002_details) | +| `gemini-1.5-pro-latest` | ✅ | ✅ | ✅ | 2,000,000 | 2025-06-20 | [View](#google_ai_gemini_1.5_pro_latest_details) | +| `gemini-2.5-flash-preview-04-17-thinking` | ✅ | ✅ | ✅ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.5_flash_preview_04_17_thinking_details) | +| `gemini-2.5-pro-preview-03-25` | ✅ | ✅ | ✅ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.5_pro_preview_03_25_details) | +| `gemini-2.5-pro-preview-05-06` | ✅ | ✅ | ✅ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.5_pro_preview_05_06_details) | +| `gemini-2.5-flash-preview-05-20` | ✅ | ⚠️ | ✅ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.5_flash_preview_05_20_details) | +| `gemini-2.0-flash-thinking-exp` | ⚠️ | ✅ | ✅ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_flash_thinking_exp_details) | +| `gemini-2.0-flash-thinking-exp-1219` | ⚠️ | ✅ | ✅ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_flash_thinking_exp_1219_details) | +| `gemini-2.0-flash-thinking-exp-01-21` | ⚠️ | ✅ | ⚠️ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_flash_thinking_exp_01_21_details) | +| `gemini-2.5-flash-preview-04-17` | ⚠️ | ✅ | ⚠️ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.5_flash_preview_04_17_details) | +| `gemini-2.5-pro-preview-06-05` | ⚠️ | ✅ | ⚠️ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.5_pro_preview_06_05_details) | +| `gemini-1.0-pro-vision-latest` | ❌ | ❌ | ❌ | 12,288 | 2025-06-20 | [View](#google_ai_gemini_1.0_pro_vision_latest_details) | +| `gemini-1.5-flash` | ❌ | ❌ | ❌ | 1,000,000 | 2025-06-20 | [View](#google_ai_gemini_1.5_flash_details) | +| `gemini-1.5-flash-002` | ❌ | ❌ | ❌ | 1,000,000 | 2025-06-20 | [View](#google_ai_gemini_1.5_flash_002_details) | +| `gemini-1.5-flash-8b` | ❌ | ❌ | ❌ | 1,000,000 | 2025-06-20 | [View](#google_ai_gemini_1.5_flash_8b_details) | +| `gemini-1.5-flash-8b-001` | ❌ | ❌ | ❌ | 1,000,000 | 2025-06-20 | [View](#google_ai_gemini_1.5_flash_8b_001_details) | +| `gemini-1.5-flash-8b-latest` | ❌ | ❌ | ❌ | 1,000,000 | 2025-06-20 | [View](#google_ai_gemini_1.5_flash_8b_latest_details) | +| `gemini-1.5-flash-latest` | ❌ | ❌ | ❌ | 1,000,000 | 2025-06-20 | [View](#google_ai_gemini_1.5_flash_latest_details) | +| `gemini-2.0-flash` | ❌ | ❌ | ❌ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_flash_details) | +| `gemini-2.0-flash-001` | ❌ | ❌ | ❌ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_flash_001_details) | +| `gemini-2.0-flash-exp` | ❌ | ❌ | ❌ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_flash_exp_details) | +| `gemini-2.0-flash-exp-image-generation` | ❌ | ❌ | ❌ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_flash_exp_image_generation_details) | +| `gemini-2.0-flash-lite` | ❌ | ❌ | ❌ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_flash_lite_details) | +| `gemini-2.0-flash-lite-001` | ❌ | ❌ | ❌ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_flash_lite_001_details) | +| `gemini-2.0-flash-lite-preview` | ❌ | ❌ | ❌ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_flash_lite_preview_details) | +| `gemini-2.0-flash-lite-preview-02-05` | ❌ | ❌ | ❌ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_flash_lite_preview_02_05_details) | +| `gemini-2.0-flash-preview-image-generation` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#google_ai_gemini_2.0_flash_preview_image_generation_details) | +| `gemini-2.0-pro-exp` | ❌ | ❌ | ❌ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_pro_exp_details) | +| `gemini-2.0-pro-exp-02-05` | ❌ | ❌ | ❌ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.0_pro_exp_02_05_details) | +| `gemini-2.5-flash-preview-tts` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#google_ai_gemini_2.5_flash_preview_tts_details) | +| `gemini-2.5-pro-exp-03-25` | ❌ | ❌ | ❌ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_2.5_pro_exp_03_25_details) | +| `gemini-2.5-pro-preview-tts` | ❌ | ❌ | ❌ | 65,536 | 2025-06-20 | [View](#google_ai_gemini_2.5_pro_preview_tts_details) | +| `gemini-exp-1206` | ❌ | ❌ | ❌ | 1,048,576 | 2025-06-20 | [View](#google_ai_gemini_exp_1206_details) | +| `gemini-pro-vision` | ❌ | ❌ | ❌ | 12,288 | 2025-06-20 | [View](#google_ai_gemini_pro_vision_details) | + + +## letta + +| Model | Basic | Token Streaming | Multimodal | Context Window | Last Scanned | Details | +|---------------------------------------------------|:---:|:-------------:|:--------:|:------------:|:----------:|:-----:| +| `letta-free` | ⚠️ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#letta_letta_free_details) | + + +## together + +| Model | Basic | Token Streaming | Multimodal | Context Window | Last Scanned | Details | +|---------------------------------------------------|:---:|:-------------:|:--------:|:------------:|:----------:|:-----:| +| `Qwen/Qwen2.5-72B-Instruct-Turbo` | ✅ | ✅ | ⚠️ | 131,072 | 2025-06-20 | [View](#together_qwen_qwen2.5_72b_instruct_turbo_details) | +| `arcee-ai/virtuoso-large` | ⚠️ | ✅ | ✅ | 131,072 | 2025-06-20 | [View](#together_arcee_ai_virtuoso_large_details) | +| `Qwen/QwQ-32B` | ⚠️ | ✅ | ⚠️ | 131,072 | 2025-06-20 | [View](#together_qwen_qwq_32b_details) | +| `Qwen/Qwen2.5-7B-Instruct-Turbo` | ⚠️ | ✅ | ⚠️ | 32,768 | 2025-06-20 | [View](#together_qwen_qwen2.5_7b_instruct_turbo_details) | +| `Qwen/Qwen2.5-Coder-32B-Instruct` | ⚠️ | ✅ | ⚠️ | 16,384 | 2025-06-20 | [View](#together_qwen_qwen2.5_coder_32b_instruct_details) | +| `arcee-ai/coder-large` | ⚠️ | ✅ | ⚠️ | 32,768 | 2025-06-20 | [View](#together_arcee_ai_coder_large_details) | +| `arcee_ai/arcee-spotlight` | ⚠️ | ✅ | ⚠️ | 131,072 | 2025-06-20 | [View](#together_arcee_ai_arcee_spotlight_details) | +| `meta-llama/Llama-3.2-3B-Instruct-Turbo` | ⚠️ | ✅ | ❌ | 131,072 | 2025-06-20 | [View](#together_meta_llama_llama_3.2_3b_instruct_turbo_details) | +| `meta-llama/Llama-3.3-70B-Instruct-Turbo` | ⚠️ | ✅ | ❌ | 131,072 | 2025-06-20 | [View](#together_meta_llama_llama_3.3_70b_instruct_turbo_details) | +| `meta-llama/Llama-3.3-70B-Instruct-Turbo-Free` | ⚠️ | ✅ | ❌ | 131,072 | 2025-06-20 | [View](#together_meta_llama_llama_3.3_70b_instruct_turbo_free_details) | +| `meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo` | ⚠️ | ✅ | ❌ | 130,815 | 2025-06-20 | [View](#together_meta_llama_meta_llama_3.1_405b_instruct_turbo_details) | +| `meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo` | ⚠️ | ✅ | ❌ | 131,072 | 2025-06-20 | [View](#together_meta_llama_meta_llama_3.1_70b_instruct_turbo_details) | +| `nvidia/Llama-3.1-Nemotron-70B-Instruct-HF` | ⚠️ | ✅ | ❌ | 32,768 | 2025-06-20 | [View](#together_nvidia_llama_3.1_nemotron_70b_instruct_hf_details) | +| `arcee-ai/virtuoso-medium-v2` | ⚠️ | ⚠️ | ✅ | 131,072 | 2025-06-20 | [View](#together_arcee_ai_virtuoso_medium_v2_details) | +| `meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8` | ⚠️ | ❌ | ✅ | 1,048,576 | 2025-06-20 | [View](#together_meta_llama_llama_4_maverick_17b_128e_instruct_fp8_details) | +| `Qwen/Qwen3-235B-A22B-fp8-tput` | ⚠️ | ⚠️ | ❌ | 40,960 | 2025-06-20 | [View](#together_qwen_qwen3_235b_a22b_fp8_tput_details) | +| `deepseek-ai/DeepSeek-V3` | ⚠️ | ⚠️ | ❌ | 131,072 | 2025-06-20 | [View](#together_deepseek_ai_deepseek_v3_details) | +| `meta-llama/Llama-4-Scout-17B-16E-Instruct` | ⚠️ | ⚠️ | ❌ | 1,048,576 | 2025-06-20 | [View](#together_meta_llama_llama_4_scout_17b_16e_instruct_details) | +| `meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo` | ⚠️ | ⚠️ | ❌ | 131,072 | 2025-06-20 | [View](#together_meta_llama_meta_llama_3.1_8b_instruct_turbo_details) | +| `mistralai/Mixtral-8x7B-Instruct-v0.1` | ⚠️ | ⚠️ | ❌ | 32,768 | 2025-06-20 | [View](#together_mistralai_mixtral_8x7b_instruct_v0.1_details) | +| `arcee-ai/caller` | ❌ | ⚠️ | ❌ | 32,768 | 2025-06-20 | [View](#together_arcee_ai_caller_details) | +| `mistralai/Mistral-Small-24B-Instruct-2501` | ❌ | ⚠️ | ❌ | 32,768 | 2025-06-20 | [View](#together_mistralai_mistral_small_24b_instruct_2501_details) | +| `NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#together_nousresearch_nous_hermes_2_mixtral_8x7b_dpo_details) | +| `Qwen/Qwen2-72B-Instruct` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#together_qwen_qwen2_72b_instruct_details) | +| `Qwen/Qwen2-VL-72B-Instruct` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#together_qwen_qwen2_vl_72b_instruct_details) | +| `Qwen/Qwen2.5-VL-72B-Instruct` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#together_qwen_qwen2.5_vl_72b_instruct_details) | +| `arcee-ai/arcee-blitz` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#together_arcee_ai_arcee_blitz_details) | +| `arcee-ai/maestro-reasoning` | ❌ | ❌ | ❌ | 131,072 | 2025-06-20 | [View](#together_arcee_ai_maestro_reasoning_details) | +| `deepseek-ai/DeepSeek-R1` | ❌ | ❌ | ❌ | 163,840 | 2025-06-20 | [View](#together_deepseek_ai_deepseek_r1_details) | +| `deepseek-ai/DeepSeek-R1-Distill-Llama-70B` | ❌ | ❌ | ❌ | 131,072 | 2025-06-20 | [View](#together_deepseek_ai_deepseek_r1_distill_llama_70b_details) | +| `deepseek-ai/DeepSeek-R1-Distill-Llama-70B-free` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#together_deepseek_ai_deepseek_r1_distill_llama_70b_free_details) | +| `deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B` | ❌ | ❌ | ❌ | 131,072 | 2025-06-20 | [View](#together_deepseek_ai_deepseek_r1_distill_qwen_1.5b_details) | +| `deepseek-ai/DeepSeek-R1-Distill-Qwen-14B` | ❌ | ❌ | ❌ | 131,072 | 2025-06-20 | [View](#together_deepseek_ai_deepseek_r1_distill_qwen_14b_details) | +| `deepseek-ai/DeepSeek-V3-p-dp` | ❌ | ❌ | ❌ | 131,072 | 2025-06-20 | [View](#together_deepseek_ai_deepseek_v3_p_dp_details) | +| `google/gemma-2-27b-it` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#together_google_gemma_2_27b_it_details) | +| `lgai/exaone-3-5-32b-instruct` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#together_lgai_exaone_3_5_32b_instruct_details) | +| `lgai/exaone-deep-32b` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#together_lgai_exaone_deep_32b_details) | +| `marin-community/marin-8b-instruct` | ❌ | ❌ | ❌ | 131,072 | 2025-06-20 | [View](#together_marin_community_marin_8b_instruct_details) | +| `meta-llama/Llama-3-70b-chat-hf` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#together_meta_llama_llama_3_70b_chat_hf_details) | +| `meta-llama/Llama-3-8b-chat-hf` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#together_meta_llama_llama_3_8b_chat_hf_details) | +| `meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo` | ❌ | ❌ | ❌ | 131,072 | 2025-06-20 | [View](#together_meta_llama_llama_3.2_11b_vision_instruct_turbo_details) | +| `meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo` | ❌ | ❌ | ❌ | 131,072 | 2025-06-20 | [View](#together_meta_llama_llama_3.2_90b_vision_instruct_turbo_details) | +| `meta-llama/Llama-Vision-Free` | ❌ | ❌ | ❌ | 131,072 | 2025-06-20 | [View](#together_meta_llama_llama_vision_free_details) | +| `meta-llama/Meta-Llama-3-70B-Instruct-Turbo` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#together_meta_llama_meta_llama_3_70b_instruct_turbo_details) | +| `meta-llama/Meta-Llama-3-8B-Instruct-Lite` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#together_meta_llama_meta_llama_3_8b_instruct_lite_details) | +| `mistralai/Mistral-7B-Instruct-v0.1` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#together_mistralai_mistral_7b_instruct_v0.1_details) | +| `mistralai/Mistral-7B-Instruct-v0.2` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#together_mistralai_mistral_7b_instruct_v0.2_details) | +| `mistralai/Mistral-7B-Instruct-v0.3` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#together_mistralai_mistral_7b_instruct_v0.3_details) | +| `perplexity-ai/r1-1776` | ❌ | ❌ | ❌ | 163,840 | 2025-06-20 | [View](#together_perplexity_ai_r1_1776_details) | +| `scb10x/scb10x-llama3-1-typhoon2-70b-instruct` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#together_scb10x_scb10x_llama3_1_typhoon2_70b_instruct_details) | +| `scb10x/scb10x-typhoon-2-1-gemma3-12b` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#together_scb10x_scb10x_typhoon_2_1_gemma3_12b_details) | +| `togethercomputer/MoA-1` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#together_togethercomputer_moa_1_details) | +| `togethercomputer/MoA-1-Turbo` | ❌ | ❌ | ❌ | 32,768 | 2025-06-20 | [View](#together_togethercomputer_moa_1_turbo_details) | +| `togethercomputer/Refuel-Llm-V2` | ❌ | ❌ | ❌ | 16,384 | 2025-06-20 | [View](#together_togethercomputer_refuel_llm_v2_details) | +| `togethercomputer/Refuel-Llm-V2-Small` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#together_togethercomputer_refuel_llm_v2_small_details) | + + +## deepseek + +| Model | Basic | Token Streaming | Multimodal | Context Window | Last Scanned | Details | +|---------------------------------------------------|:---:|:-------------:|:--------:|:------------:|:----------:|:-----:| +| `deepseek-chat` | ❌ | ❌ | ❌ | 64,000 | 2025-06-20 | [View](#deepseek_deepseek_chat_details) | +| `deepseek-reasoner` | ❌ | ❌ | ❌ | 64,000 | 2025-06-20 | [View](#deepseek_deepseek_reasoner_details) | + + +## groq + +| Model | Basic | Token Streaming | Multimodal | Context Window | Last Scanned | Details | +|---------------------------------------------------|:---:|:-------------:|:--------:|:------------:|:----------:|:-----:| +| `allam-2-7b` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_allam_2_7b_details) | +| `compound-beta` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_compound_beta_details) | +| `compound-beta-mini` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_compound_beta_mini_details) | +| `deepseek-r1-distill-llama-70b` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_deepseek_r1_distill_llama_70b_details) | +| `distil-whisper-large-v3-en` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_distil_whisper_large_v3_en_details) | +| `gemma2-9b-it` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_gemma2_9b_it_details) | +| `llama-3.1-8b-instant` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_llama_3.1_8b_instant_details) | +| `llama-3.3-70b-versatile` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_llama_3.3_70b_versatile_details) | +| `llama-guard-3-8b` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_llama_guard_3_8b_details) | +| `llama3-70b-8192` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_llama3_70b_8192_details) | +| `llama3-8b-8192` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_llama3_8b_8192_details) | +| `meta-llama/llama-4-maverick-17b-128e-instruct` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_meta_llama_llama_4_maverick_17b_128e_instruct_details) | +| `meta-llama/llama-4-scout-17b-16e-instruct` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_meta_llama_llama_4_scout_17b_16e_instruct_details) | +| `meta-llama/llama-guard-4-12b` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_meta_llama_llama_guard_4_12b_details) | +| `meta-llama/llama-prompt-guard-2-22m` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_meta_llama_llama_prompt_guard_2_22m_details) | +| `meta-llama/llama-prompt-guard-2-86m` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_meta_llama_llama_prompt_guard_2_86m_details) | +| `mistral-saba-24b` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_mistral_saba_24b_details) | +| `playai-tts` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_playai_tts_details) | +| `playai-tts-arabic` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_playai_tts_arabic_details) | +| `qwen-qwq-32b` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_qwen_qwq_32b_details) | +| `qwen/qwen3-32b` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_qwen_qwen3_32b_details) | +| `whisper-large-v3` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_whisper_large_v3_details) | +| `whisper-large-v3-turbo` | ❌ | ❌ | ❌ | 8,192 | 2025-06-20 | [View](#groq_whisper_large_v3_turbo_details) | + + +--- + +# Detailed Test Results + +## anthropic + + +### claude-3-5-haiku-20241022 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### claude-3-5-sonnet-20241022 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### claude-3-7-sonnet-20250219 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### claude-sonnet-4-20250514 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### claude-opus-4-20250514 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### claude-3-5-sonnet-20240620 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### claude-3-haiku-20240307 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### claude-3-opus-20240229 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### claude-3-sonnet-20240229 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + + +## openai + + +### gpt-4.1 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4.1-2025-04-14 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4.1-nano-2025-04-14 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4o + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4o-2024-05-13 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4-turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4.1-mini + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4.5-preview + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4.5-preview-2025-02-27 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4o-2024-08-06 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4-0613 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4-1106-preview + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4-turbo-2024-04-09 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4.1-mini-2025-04-14 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4.1-nano + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4o-2024-11-20 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4-turbo-preview + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4-0125-preview + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4o-mini + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4o-mini-2024-07-18 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gpt-4 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### o1 + +### Basic + +- ❌ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### o1-2024-12-17 + +### Basic + +- ❌ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### o3 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### o3-2025-04-16 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### o3-mini + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### o3-mini-2025-01-31 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### o3-pro + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### o3-pro-2025-06-10 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + + +## google_ai + + +### gemini-1.5-pro + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-1.5-pro-002 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-1.5-pro-latest + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.5-flash-preview-04-17-thinking + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.5-pro-preview-03-25 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.5-pro-preview-05-06 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.5-flash-preview-05-20 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-flash-thinking-exp + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-flash-thinking-exp-1219 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-flash-thinking-exp-01-21 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.5-flash-preview-04-17 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.5-pro-preview-06-05 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-1.0-pro-vision-latest + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-1.5-flash + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-1.5-flash-002 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-1.5-flash-8b + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-1.5-flash-8b-001 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-1.5-flash-8b-latest + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-1.5-flash-latest + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-flash + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-flash-001 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-flash-exp + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-flash-exp-image-generation + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-flash-lite + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-flash-lite-001 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-flash-lite-preview + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-flash-lite-preview-02-05 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-flash-preview-image-generation + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-pro-exp + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.0-pro-exp-02-05 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.5-flash-preview-tts + +### Basic + +- ❌ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.5-pro-exp-03-25 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-2.5-pro-preview-tts + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-exp-1206 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemini-pro-vision + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + + +## letta + + +### letta-free + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + + +## together + + +### Qwen/Qwen2.5-72B-Instruct-Turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### arcee-ai/virtuoso-large + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### Qwen/QwQ-32B + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### Qwen/Qwen2.5-7B-Instruct-Turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### Qwen/Qwen2.5-Coder-32B-Instruct + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### arcee-ai/coder-large + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### arcee_ai/arcee-spotlight + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Llama-3.2-3B-Instruct-Turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Llama-3.3-70B-Instruct-Turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Llama-3.3-70B-Instruct-Turbo-Free + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### nvidia/Llama-3.1-Nemotron-70B-Instruct-HF + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ✅ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### arcee-ai/virtuoso-medium-v2 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ✅ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ✅ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ✅ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### Qwen/Qwen3-235B-A22B-fp8-tput + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### deepseek-ai/DeepSeek-V3 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Llama-4-Scout-17B-16E-Instruct + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ✅ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### mistralai/Mixtral-8x7B-Instruct-v0.1 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ✅ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ✅ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ✅ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ✅ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ✅ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ✅ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### arcee-ai/caller + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ✅ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### mistralai/Mistral-Small-24B-Instruct-2501 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ✅ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### Qwen/Qwen2-72B-Instruct + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### Qwen/Qwen2-VL-72B-Instruct + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### Qwen/Qwen2.5-VL-72B-Instruct + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### arcee-ai/arcee-blitz + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### arcee-ai/maestro-reasoning + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### deepseek-ai/DeepSeek-R1 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### deepseek-ai/DeepSeek-R1-Distill-Llama-70B + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### deepseek-ai/DeepSeek-R1-Distill-Llama-70B-free + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### deepseek-ai/DeepSeek-R1-Distill-Qwen-14B + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### deepseek-ai/DeepSeek-V3-p-dp + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### google/gemma-2-27b-it + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### lgai/exaone-3-5-32b-instruct + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### lgai/exaone-deep-32b + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### marin-community/marin-8b-instruct + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Llama-3-70b-chat-hf + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Llama-3-8b-chat-hf + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Llama-Vision-Free + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Meta-Llama-3-70B-Instruct-Turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/Meta-Llama-3-8B-Instruct-Lite + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### mistralai/Mistral-7B-Instruct-v0.1 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### mistralai/Mistral-7B-Instruct-v0.2 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### mistralai/Mistral-7B-Instruct-v0.3 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### perplexity-ai/r1-1776 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### scb10x/scb10x-llama3-1-typhoon2-70b-instruct + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### scb10x/scb10x-typhoon-2-1-gemma3-12b + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### togethercomputer/MoA-1 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### togethercomputer/MoA-1-Turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### togethercomputer/Refuel-Llm-V2 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### togethercomputer/Refuel-Llm-V2-Small + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + + +## deepseek + + +### deepseek-chat + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### deepseek-reasoner + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + + +## groq + + +### allam-2-7b + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### compound-beta + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### compound-beta-mini + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### deepseek-r1-distill-llama-70b + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### distil-whisper-large-v3-en + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### gemma2-9b-it + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### llama-3.1-8b-instant + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### llama-3.3-70b-versatile + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### llama-guard-3-8b + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### llama3-70b-8192 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### llama3-8b-8192 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/llama-4-maverick-17b-128e-instruct + +### Basic + +- ❌ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/llama-4-scout-17b-16e-instruct + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/llama-guard-4-12b + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/llama-prompt-guard-2-22m + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### meta-llama/llama-prompt-guard-2-86m + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### mistral-saba-24b + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### playai-tts + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### playai-tts-arabic + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### qwen-qwq-32b + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### qwen/qwen3-32b + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### whisper-large-v3 + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) + + +### whisper-large-v3-turbo + +### Basic + +- ✅ [`test_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L463) +- ❌ [`test_async_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L704) +- ❌ [`test_auto_summarize`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L731) +- ❌ [`test_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L345) +- ❌ [`test_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L368) +- ✅ [`test_step_stream_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L566) +- ❌ [`test_step_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L488) +- ❌ [`test_step_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L513) +- ❌ [`test_step_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L539) +- ❌ [`test_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L392) + +### Token Streaming + +- ✅ [`test_token_streaming_agent_loop_error`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L674) +- ❌ [`test_token_streaming_greeting_with_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L593) +- ❌ [`test_token_streaming_greeting_without_assistant_message`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L619) +- ❌ [`test_token_streaming_tool_call`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L646) + +### Multimodal + +- ❌ [`test_base64_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L440) +- ❌ [`test_url_image_input`](https://github.com/letta-ai/letta/blob/main/.github/scripts/model-sweep/model_sweep.py#L417) diff --git a/.github/workflows/alembic-validation.yml b/.github/workflows/alembic-validation.yml new file mode 100644 index 0000000..32b0139 --- /dev/null +++ b/.github/workflows/alembic-validation.yml @@ -0,0 +1,123 @@ +name: Alembic Migration Validation + +on: + pull_request: + branches: [ main ] + pull_request_target: + branches: [ main ] + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + changed-files: + # Run on pull_request OR on pull_request_target only when labeled "safe to test" + if: github.event_name == 'pull_request' || (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe to test')) + runs-on: ubuntu-latest + name: changed-files + outputs: + all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }} + any_changed: ${{ steps.changed-files.outputs.any_changed }} + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v44 + with: + files: | + alembic/** + .github/workflows/alembic-validation.yml + + test-sqlite: + needs: [ changed-files ] + if: ${{ needs.changed-files.outputs.any_changed == 'true' }} + runs-on: [self-hosted, medium] + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + + - name: Install dependencies + shell: bash + working-directory: . + run: uv sync --no-install-project ${{ inputs.install-args || '--extra sqlite --extra external-tools --extra dev --extra cloud-tool-sandbox' }} + - name: Test alembic migration + working-directory: . + run: | + uv run alembic upgrade head + # kinda janky but I think this might not matter for sqlite? + # uv run alembic check + + - name: Cleanup persistent data + if: ${{ always() }} + working-directory: . + run: | + echo "Cleaning up persistent data..." + sudo rm -rf ~/.letta || true + + test-postgres: + needs: [ changed-files ] + if: ${{ needs.changed-files.outputs.any_changed == 'true' }} + runs-on: [self-hosted, medium] + timeout-minutes: 15 + services: + postgres: + image: pgvector/pgvector:pg17 + ports: + - 5432:5432 + env: + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + + - name: Install dependencies + shell: bash + working-directory: . + run: uv sync --no-install-project ${{ inputs.install-args || '--extra postgres --extra external-tools --extra dev --extra cloud-tool-sandbox' }} + - name: Test alembic migration + working-directory: . + env: + LETTA_PG_PORT: 5432 + LETTA_PG_USER: postgres + LETTA_PG_PASSWORD: postgres + LETTA_PG_DB: postgres + LETTA_PG_HOST: localhost + run: | + psql -h localhost -U postgres -d postgres -c 'CREATE EXTENSION IF NOT EXISTS vector;' + uv run alembic upgrade head + uv run alembic check + + - name: Print docker logs if tests fail + if: ${{ failure() || cancelled() }} + run: | + echo "Printing Docker Logs..." + docker logs $(docker ps -aq --filter "ancestor=pgvector/pgvector:pg17") || true + + - name: Cleanup containers and volumes + if: ${{ always() }} + run: | + echo "Cleaning up containers and volumes..." + docker stop $(docker ps -aq --filter "ancestor=pgvector/pgvector:pg17") || true + docker rm $(docker ps -aq --filter "ancestor=pgvector/pgvector:pg17") || true + docker volume prune -f || true + docker system prune -f || true diff --git a/.github/workflows/close_stale_issues.yml b/.github/workflows/close_stale_issues.yml new file mode 100644 index 0000000..d5cd3cf --- /dev/null +++ b/.github/workflows/close_stale_issues.yml @@ -0,0 +1,22 @@ +name: Close inactive issues +on: + schedule: + - cron: "30 1 * * *" + +jobs: + close-issues: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v5 + with: + days-before-issue-stale: 30 + days-before-issue-close: 14 + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." + close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." + days-before-pr-stale: -1 + days-before-pr-close: -1 + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/core-integration-tests.yml b/.github/workflows/core-integration-tests.yml new file mode 100644 index 0000000..c8cc489 --- /dev/null +++ b/.github/workflows/core-integration-tests.yml @@ -0,0 +1,48 @@ +name: 🐍🧪 [Core] Integration Tests + +on: + pull_request: + branches: + - main + pull_request_target: + branches: + - main + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + integration-tests: + # Run on pull_request OR on pull_request_target only when labeled "safe to test" + if: github.event_name == 'pull_request' || (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe to test')) + uses: ./.github/workflows/reusable-test-workflow.yml + with: + test-type: 'integration' + use-redis: true + is-external-pr: ${{ github.event_name == 'pull_request_target' && !contains(github.event.pull_request.labels.*.name, 'safe to test') }} + changed-files-pattern: | + ** + .github/workflows/reusable-test-workflow.yml + .github/workflows/core-integration-tests.yml + install-args: '--extra postgres --extra external-tools --extra dev --extra cloud-tool-sandbox' + timeout-minutes: 15 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + matrix-strategy: | + { + "fail-fast": false, + "matrix": { + "test_suite": [ + "integration_test_async_tool_sandbox.py", + "integration_test_sleeptime_agent.py", + "integration_test_agent_tool_graph.py", + "integration_test_multi_agent.py", + "integration_test_batch_api_cron_jobs.py", + "integration_test_builtin_tools.py", + "integration_test_turbopuffer.py", + "integration_test_human_in_the_loop.py" + ] + } + } + secrets: inherit diff --git a/.github/workflows/core-lint.yml b/.github/workflows/core-lint.yml new file mode 100644 index 0000000..d020163 --- /dev/null +++ b/.github/workflows/core-lint.yml @@ -0,0 +1,67 @@ +name: 🐍🧹 [Core] Lint and Test + +on: + pull_request: + branches: [ main ] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + changed-files: + runs-on: ubuntu-latest + name: changed-files + outputs: + all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }} + any_changed: ${{ steps.changed-files.outputs.any_changed }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v44 + with: + files: | + letta/** + tests/** + *.py + pyproject.toml + .github/workflows/core-lint.yml + main: + needs: [ changed-files ] + if: ${{ needs.changed-files.outputs.any_changed == 'true' }} + runs-on: [self-hosted, medium] + strategy: + matrix: + python-version: ["3.12"] # Adjust Python version matrix if needed + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + shell: bash + working-directory: . + run: uv sync --no-install-project ${{ inputs.install-args || '--extra postgres --extra external-tools --extra dev --extra cloud-tool-sandbox' }} + - name: Validate PR Title + if: github.event_name == 'pull_request' + uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Pyright + uses: jakebailey/pyright-action@v2 + with: + python-version: ${{ matrix.python-version }} + level: "error" + continue-on-error: true + + - name: Run Ruff Check + working-directory: . + run: uv run ruff check --config pyproject.toml --diff . + + - name: Run Ruff Format + working-directory: . + run: uv run ruff format --config pyproject.toml --check --diff . diff --git a/.github/workflows/core-unit-sqlite-test.yaml b/.github/workflows/core-unit-sqlite-test.yaml new file mode 100644 index 0000000..87ff951 --- /dev/null +++ b/.github/workflows/core-unit-sqlite-test.yaml @@ -0,0 +1,60 @@ +name: 🐍👨‍🔬 [Core] Unit Tests (SQLite) + +on: + pull_request: + branches: + - main + pull_request_target: + branches: + - main + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + unit-tests: + # Run on pull_request OR on pull_request_target only when labeled "safe to test" + if: github.event_name == 'pull_request' || (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe to test')) + uses: ./.github/workflows/reusable-test-workflow.yml + with: + test-type: 'sqlite' + use-redis: true + changed-files-pattern: | + apps/core/** + .github/workflows/reusable-test-workflow.yml + .github/workflows/core-unit-sqlite-test.yml + install-args: '--extra postgres --extra external-tools --extra dev --extra cloud-tool-sandbox --extra sqlite' + timeout-minutes: 15 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + matrix-strategy: | + { + "fail-fast": false, + "matrix": { + "include": [ + {"test_suite": "test_client.py"}, + {"test_suite": "test_sdk_client.py"}, + {"test_suite": "test_tool_schema_parsing.py"}, + {"test_suite": "test_tool_rule_solver.py"}, + {"test_suite": "test_memory.py"}, + {"test_suite": "test_utils.py"}, + {"test_suite": "test_stream_buffer_readers.py"}, + {"test_suite": "test_optimistic_json_parser.py"}, + {"test_suite": "test_llm_clients.py"}, + {"test_suite": "test_letta_agent_batch.py"}, + {"test_suite": "test_providers.py"}, + {"test_suite": "test_sources.py"}, + {"test_suite": "managers/"}, + {"test_suite": "sdk/"}, + {"test_suite": "mcp_tests/"}, + {"test_suite": "test_timezone_formatting.py"}, + {"test_suite": "test_plugins.py"}, + {"test_suite": "test_embeddings.py"}, + {"test_suite": "test_crypto_utils.py"}, + {"test_suite": "test_mcp_encryption.py"}, + {"test_suite": "test_secret.py"} + ] + } + } + secrets: inherit diff --git a/.github/workflows/core-unit-test.yml b/.github/workflows/core-unit-test.yml new file mode 100644 index 0000000..9b633b8 --- /dev/null +++ b/.github/workflows/core-unit-test.yml @@ -0,0 +1,63 @@ +name: 🐍👨‍🔬 [Core] Unit Tests + +on: + pull_request: + branches: + - main + pull_request_target: + branches: + - main + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + unit-tests: + # Run on pull_request OR on pull_request_target only when labeled "safe to test" + if: github.event_name == 'pull_request' || (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe to test')) + uses: ./.github/workflows/reusable-test-workflow.yml + with: + test-type: 'unit' + use-redis: true + is-external-pr: ${{ github.event_name == 'pull_request_target' && !contains(github.event.pull_request.labels.*.name, 'safe to test') }} + changed-files-pattern: | + ** + .github/workflows/reusable-test-workflow.yml + .github/workflows/core-unit-test.yml + install-args: '--extra postgres --extra external-tools --extra dev --extra cloud-tool-sandbox' + timeout-minutes: 15 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + matrix-strategy: | + { + "fail-fast": false, + "matrix": { + "include": [ + {"test_suite": "test_client.py"}, + {"test_suite": "test_sdk_client.py"}, + {"test_suite": "managers/"}, + {"test_suite": "test_tool_schema_parsing.py"}, + {"test_suite": "test_tool_rule_solver.py"}, + {"test_suite": "test_memory.py"}, + {"test_suite": "test_utils.py"}, + {"test_suite": "test_stream_buffer_readers.py"}, + {"test_suite": "test_agent_serialization_v2.py"}, + {"test_suite": "test_optimistic_json_parser.py"}, + {"test_suite": "test_llm_clients.py"}, + {"test_suite": "test_letta_agent_batch.py"}, + {"test_suite": "test_providers.py"}, + {"test_suite": "test_server_providers.py"}, + {"test_suite": "test_sources.py"}, + {"test_suite": "sdk/"}, + {"test_suite": "mcp_tests/"}, + {"test_suite": "test_timezone_formatting.py"}, + {"test_suite": "test_plugins.py"}, + {"test_suite": "test_embeddings.py"}, + {"test_suite": "test_crypto_utils.py"}, + {"test_suite": "test_mcp_encryption.py"}, + {"test_suite": "test_secret.py"} + ] + } + } + secrets: inherit diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..620b793 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,40 @@ +name: Docker Image CI + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract version number + id: extract_version + run: echo "CURRENT_VERSION=$(awk -F '\"' '/version =/ { print $2 }' pyproject.toml | head -n 1)" >> $GITHUB_ENV + + - name: Build and push + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: | + letta/letta:${{ env.CURRENT_VERSION }} + letta/letta:latest + memgpt/letta:${{ env.CURRENT_VERSION }} + memgpt/letta:latest diff --git a/.github/workflows/docker-integration-tests.yaml b/.github/workflows/docker-integration-tests.yaml new file mode 100644 index 0000000..dd2c0c5 --- /dev/null +++ b/.github/workflows/docker-integration-tests.yaml @@ -0,0 +1,77 @@ +name: Run Docker integration tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + # Database configuration - these will be used by dev-compose.yaml + LETTA_PG_DB: letta + LETTA_PG_USER: letta + LETTA_PG_PASSWORD: letta + LETTA_PG_HOST: pgvector_db # Internal Docker service name + LETTA_PG_PORT: 5432 + # Server configuration for tests + LETTA_SERVER_PASS: test_server_token + LETTA_SERVER_URL: http://localhost:8283 + # API keys + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + # Additional API keys that dev-compose.yaml expects (optional) + GROQ_API_KEY: "" + ANTHROPIC_API_KEY: "" + OLLAMA_BASE_URL: "" + AZURE_API_KEY: "" + AZURE_BASE_URL: "" + AZURE_API_VERSION: "" + GEMINI_API_KEY: "" + VLLM_API_BASE: "" + OPENLLM_AUTH_TYPE: "" + OPENLLM_API_KEY: "" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up python 3.11 + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: 3.11 + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Set permissions for log directory + run: | + mkdir -p /home/runner/.letta/logs + sudo chown -R $USER:$USER /home/runner/.letta/logs + chmod -R 755 /home/runner/.letta/logs + + - name: Build and run docker dev server + run: | + # dev-compose.yaml will use the environment variables we set above + docker compose -f dev-compose.yaml up --build -d + + - name: Wait for service + run: bash scripts/wait_for_service.sh http://localhost:8283 -- echo "Service is ready" + + - name: Run tests with pytest + env: + PYTHONPATH: ${{ github.workspace }}:${{ env.PYTHONPATH }} + LETTA_PG_URI: postgresql+pg8000://${{ env.LETTA_PG_USER }}:${{ env.LETTA_PG_PASSWORD }}@localhost:${{ env.LETTA_PG_PORT }}/${{ env.LETTA_PG_DB }} + run: | + uv sync --extra dev --extra postgres --extra sqlite + uv run pytest -s tests/test_client.py + + - name: Print docker logs if tests fail + if: failure() + run: | + echo "Printing Docker Logs..." + docker compose -f dev-compose.yaml logs diff --git a/.github/workflows/fern-check.yml b/.github/workflows/fern-check.yml new file mode 100644 index 0000000..984b831 --- /dev/null +++ b/.github/workflows/fern-check.yml @@ -0,0 +1,20 @@ +name: 🌿 Fern Check + +on: + pull_request: + branches: [ main ] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + run: + runs-on: [self-hosted, small] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check API is valid + working-directory: fern + run: fern check diff --git a/.github/workflows/fern-docs-preview.yml b/.github/workflows/fern-docs-preview.yml new file mode 100644 index 0000000..dfc44fe --- /dev/null +++ b/.github/workflows/fern-docs-preview.yml @@ -0,0 +1,45 @@ +name: Preview Docs + +on: + pull_request: + branches: + - main + pull_request_target: + branches: + - main + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + run: + if: github.event_name == 'pull_request' || (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe to test')) + runs-on: [self-hosted, small] + permissions: write-all + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + submodules: true + + - name: Generate preview URL + id: generate-docs + if: github.event_name != 'pull_request_target' || contains(github.event.pull_request.labels.*.name, 'safe to test') + working-directory: fern + env: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + run: | + OUTPUT=$(fern generate --docs --preview 2>&1) || true + echo "$OUTPUT" + URL=$(echo "$OUTPUT" | grep -oP 'Published docs to \K.*(?= \()') + echo "Preview URL: $URL" + echo "🌿 Preview your docs: $URL" > preview_url.txt + + - name: Comment URL in PR + uses: thollander/actions-comment-pull-request@v3 + with: + file-path: fern/preview_url.txt diff --git a/.github/workflows/fern-docs-publish.yml b/.github/workflows/fern-docs-publish.yml new file mode 100644 index 0000000..5e64d28 --- /dev/null +++ b/.github/workflows/fern-docs-publish.yml @@ -0,0 +1,21 @@ +name: 🌿 Publish Docs + +on: + push: + branches: [ main ] + +jobs: + run: + runs-on: [self-hosted, medium] + if: ${{ github.event_name == 'push' && contains(github.ref, 'refs/heads/main') && github.run_number > 1 }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: true + + - name: Publish Docs + working-directory: . + env: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + run: fern generate --docs --log-level debug diff --git a/.github/workflows/fern-sdk-python-preview.yml b/.github/workflows/fern-sdk-python-preview.yml new file mode 100644 index 0000000..4b5c8d8 --- /dev/null +++ b/.github/workflows/fern-sdk-python-preview.yml @@ -0,0 +1,173 @@ +name: 🌿 Preview Python SDK + +on: + pull_request: + branches: + - main + pull_request_target: + branches: + - main + types: [labeled] + push: + branches: + - main + paths: + - 'fern/openapi.json' + - 'fern/openapi-overrides.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + changed-files: + # Run on pull_request OR on pull_request_target only when labeled "safe to test" + if: github.event_name == 'pull_request' || (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe to test')) + runs-on: [self-hosted, small] + name: changed-files + outputs: + all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }} + any_changed: ${{ steps.changed-files.outputs.any_changed }} + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + submodules: true + fetch-depth: 0 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v44 + with: + files: | + fern/openapi.json + fern/openapi-overrides.yml + + preview-python-sdk: + needs: [changed-files] + name: preview-python-sdk + runs-on: [self-hosted, medium] + outputs: + cache-key: ${{ steps.cache-key.outputs.key }} + services: + postgres: + image: pgvector/pgvector:pg17 + env: + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + + - name: Checkout repo + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + submodules: true + + - name: Generate cache key + id: cache-key + run: | + echo "key=sdk-${{ github.ref_name }}-${{ hashFiles('fern/*', 'pyproject.toml') }}" >> $GITHUB_OUTPUT + + - name: Try to restore SDK cache + id: restore-cache + uses: actions/cache/restore@v4 + with: + path: | + fern/.preview/fern-python-sdk/ + key: ${{ steps.cache-key.outputs.key }} + + - name: Inject env vars into environment + if: github.event_name != 'pull_request_target' || contains(github.event.pull_request.labels.*.name, 'safe to test') + working-directory: . + run: | + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ -n "$line" ]]; then + value=$(echo "$line" | cut -d= -f2-) + echo "::add-mask::$value" + echo "$line" >> $GITHUB_ENV + fi + done < <(letta_secrets_helper --env dev --service ci) + + - name: Debug environment + shell: bash + run: | + echo "=== Environment Debug ===" + echo "PATH: $PATH" + echo "USER: $(whoami)" + echo "HOME: $HOME" + echo "Shell: $SHELL" + echo "Working directory: $(pwd)" + echo "" + echo "=== UV Debug ===" + which uv || echo "uv not found in PATH" + ls -la /usr/local/bin/uv || echo "/usr/local/bin/uv not found" + ls -la /home/ci-runner/.local/bin/uv || echo "ci-runner uv not found" + echo "" + echo "=== Test uv command ===" + uv --version || echo "uv --version failed" + + - name: Install dependencies + shell: bash + working-directory: . + run: uv sync --no-install-project ${{ inputs.install-args || '--extra postgres --extra external-tools --extra dev --extra cloud-tool-sandbox' }} + + - name: Migrate database + working-directory: . + env: + LETTA_PG_PORT: 5432 + LETTA_PG_USER: postgres + LETTA_PG_PASSWORD: postgres + LETTA_PG_DB: postgres + LETTA_PG_HOST: localhost + run: | + psql -h localhost -U postgres -d postgres -c 'CREATE EXTENSION vector' + uv run alembic upgrade head + + - name: Run letta server + if: github.event_name != 'pull_request_target' || contains(github.event.pull_request.labels.*.name, 'safe to test') + working-directory: . + env: + LETTA_PG_DB: postgres + LETTA_PG_USER: postgres + LETTA_PG_PASSWORD: postgres + LETTA_PG_HOST: localhost + LETTA_PG_PORT: 5432 + OPENAI_API_KEY: ${{ env.OPENAI_API_KEY }} + E2B_SANDBOX_TEMPLATE_ID: ${{ env.E2B_SANDBOX_TEMPLATE_ID }} + run: | + # Run server in background + uv run letta server & + # Wait for server to be ready + timeout 60 bash -c 'until curl -s http://localhost:8283/health; do sleep 1; done' + + - name: Generate Python SDK Preview + if: (github.event_name != 'pull_request_target' || contains(github.event.pull_request.labels.*.name, 'safe to test')) && steps.restore-cache.outputs.cache-hit != 'true' + working-directory: . + env: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + run: | + fern generate --group python-sdk --preview + cd fern/.preview/fern-python-sdk + poetry install + poetry build --format wheel + poetry run mypy . + poetry run pytest -rP tests/custom/test_client.py --env localhost + ls -lah + + - name: Save SDK to cache + if: steps.restore-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: | + fern/.preview/fern-python-sdk/ + key: ${{ steps.cache-key.outputs.key }} diff --git a/.github/workflows/fern-sdk-python-publish.yml b/.github/workflows/fern-sdk-python-publish.yml new file mode 100644 index 0000000..390e477 --- /dev/null +++ b/.github/workflows/fern-sdk-python-publish.yml @@ -0,0 +1,50 @@ +name: 🌿 Release Python SDK + +on: + workflow_dispatch: + inputs: + version: + description: "The version of the Python SDK that you would like to release" + required: true + type: string + workflow_run: + workflows: ["🌿 Preview Python SDK"] + types: + - completed + branches: + - main + +jobs: + release: + if: | + github.event_name == 'workflow_dispatch' || + (github.event_name == 'workflow_run' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.conclusion == 'success') + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + submodules: true + + - name: Download Fern + run: npm install -g fern-api + + - name: Generate Python SDK + working-directory: . + env: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + fern generate --group python-sdk --version ${{ inputs.version }} --log-level debug + else + fern generate --group python-sdk --log-level debug + fi + + - name: Publish Docs + working-directory: . + env: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + run: fern generate --docs diff --git a/.github/workflows/fern-sdk-typescript-preview.yml b/.github/workflows/fern-sdk-typescript-preview.yml new file mode 100644 index 0000000..1a8ae5f --- /dev/null +++ b/.github/workflows/fern-sdk-typescript-preview.yml @@ -0,0 +1,117 @@ +name: 🌿 Preview TypeScript SDK + +on: + pull_request: + branches: + - main + push: + branches: + - main + paths: + - 'fern/openapi.json' + - 'fern/openapi-overrides.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + changed-files: + runs-on: [self-hosted, small] + name: changed-files + outputs: + all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }} + any_changed: ${{ steps.changed-files.outputs.any_changed }} + steps: + - uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 0 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v44 + with: + files: | + fern/openapi.json + fern/openapi-overrides.yml + preview-typescript-sdk: + if: ${{ needs.changed-files.outputs.any_changed == 'true' }} + needs: [changed-files] + runs-on: [self-hosted, medium] + services: + postgres: + image: pgvector/pgvector:pg17 + env: + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + submodules: true + + - name: Install dependencies + shell: bash + working-directory: . + run: uv sync --no-install-project ${{ inputs.install-args || '--extra postgres --extra external-tools --extra dev --extra cloud-tool-sandbox' }} + + - name: Inject env vars into environment + working-directory: . + run: | + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ -n "$line" ]]; then + value=$(echo "$line" | cut -d= -f2-) + echo "::add-mask::$value" + echo "$line" >> $GITHUB_ENV + fi + done < <(letta_secrets_helper --env dev --service ci) + + - name: Migrate database + working-directory: . + env: + LETTA_PG_PORT: 5432 + LETTA_PG_USER: postgres + LETTA_PG_PASSWORD: postgres + LETTA_PG_DB: postgres + LETTA_PG_HOST: localhost + run: | + psql -h localhost -U postgres -d postgres -c 'CREATE EXTENSION vector' + uv run alembic upgrade head + + - name: Run letta server + working-directory: . + env: + LETTA_PG_DB: postgres + LETTA_PG_USER: postgres + LETTA_PG_PASSWORD: postgres + LETTA_PG_HOST: localhost + LETTA_PG_PORT: 5432 + OPENAI_API_KEY: ${{ env.OPENAI_API_KEY }} + E2B_SANDBOX_TEMPLATE_ID: ${{ env.E2B_SANDBOX_TEMPLATE_ID }} + run: | + # Run server in background + uv run letta server & + # Wait for server to be ready + timeout 60 bash -c 'until curl -s http://localhost:8283/health; do sleep 1; done' + + - name: Generate TypeScript SDK Preview + working-directory: . + env: + LETTA_ENV: localhost + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + run: | + fern generate --group ts-sdk --preview + cd fern/.preview/fern-typescript-node-sdk + yarn install + yarn build + yarn test tests/custom.test.ts diff --git a/.github/workflows/fern-sdk-typescript-publish.yml b/.github/workflows/fern-sdk-typescript-publish.yml new file mode 100644 index 0000000..7e39cb0 --- /dev/null +++ b/.github/workflows/fern-sdk-typescript-publish.yml @@ -0,0 +1,50 @@ +name: 🌿 Release TypeScript SDK + +on: + workflow_dispatch: + inputs: + version: + description: "The version of the TypeScript SDK that you would like to release" + required: true + type: string + workflow_run: + workflows: ["🌿 Preview TypeScript SDK"] + types: + - completed + branches: + - main + +jobs: + release: + if: | + github.event_name == 'workflow_dispatch' || + (github.event_name == 'workflow_run' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.conclusion == 'success') + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + submodules: true + + - name: Download Fern + run: npm install -g fern-api + + - name: Generate TypeScript SDK + working-directory: . + env: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + fern generate --group ts-sdk --version ${{ inputs.version }} --log-level debug + else + fern generate --group ts-sdk --log-level debug + fi + + - name: Publish Docs + working-directory: . + env: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + run: fern generate --docs diff --git a/.github/workflows/issue-guard.yml b/.github/workflows/issue-guard.yml new file mode 100644 index 0000000..22a3a31 --- /dev/null +++ b/.github/workflows/issue-guard.yml @@ -0,0 +1,168 @@ +name: "Issue Guard" + +on: + issues: + types: [opened] + +permissions: + issues: write + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: .github/TRUSTED_CONTRIBUTORS + + - name: Check issue compliance + uses: actions/github-script@v7 + with: + script: | + const issue = context.payload.issue; + const author = issue.user.login; + const body = issue.body || ''; + + // --- Allowlist checks --- + + // 1. Bots are allowed (e.g. dependabot, renovate) + if (issue.user.type === 'Bot') { + console.log(`Skipping: ${author} is a bot`); + return; + } + + // 2. Check if author has write+ access to the repo. + // This catches org members who have repo access via teams, + // and works with the default GITHUB_TOKEN (no extra scopes). + try { + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: author, + }); + if (['admin', 'write', 'maintain'].includes(data.permission)) { + console.log(`Skipping: ${author} has '${data.permission}' permission on repo`); + return; + } + } catch (e) { + console.log(`Collaborator check failed (${e.status}), continuing`); + } + + // 3. Check org membership via public membership API (no auth needed). + // Catches org members even if they don't have direct repo access. + try { + await github.rest.orgs.checkPublicMembershipForUser({ + org: 'letta-ai', + username: author, + }); + console.log(`Skipping: ${author} is a public letta-ai org member`); + return; + } catch (e) { + // 404 = not a public member + } + + // 4. Check TRUSTED_CONTRIBUTORS file + const fs = require('fs'); + try { + const trusted = fs.readFileSync('.github/TRUSTED_CONTRIBUTORS', 'utf8') + .split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + if (trusted.includes(author)) { + console.log(`Skipping: ${author} is in TRUSTED_CONTRIBUTORS`); + return; + } + } catch (e) { + console.log('No TRUSTED_CONTRIBUTORS file found, continuing'); + } + + // --- Validation checks --- + + const failures = []; + + // Check 1: Magic phrase + const magicPhrase = 'I have read the AI policy and I confirm this issue was reviewed by a human.'; + if (!body.includes(magicPhrase)) { + failures.push('Missing human verification phrase'); + } + + // Check 2: AI disclosure checkbox (the required one: "I have read the AI Policy") + // YAML form checkboxes render as "- [x] text" when checked. + const aiPolicyChecked = /- \[[xX]\] I have read the \[AI Policy\]/.test(body); + if (!aiPolicyChecked) { + failures.push('AI Policy acknowledgment checkbox not checked'); + } + + // Check 3: At least one of the two disclosure options is checked + const humanWritten = /- \[[xX]\] This issue was written entirely by a human/.test(body); + const aiAssisted = /- \[[xX]\] This issue was written with AI assistance/.test(body); + if (!humanWritten && !aiAssisted) { + failures.push('No AI disclosure option selected (must indicate whether issue is human-written or AI-assisted)'); + } + + if (failures.length === 0) { + console.log(`Issue #${issue.number} passed all checks`); + return; + } + + // --- Close and lock --- + + console.log(`Issue #${issue.number} failed checks: ${failures.join(', ')}`); + + const comment = [ + `**This issue was automatically closed** because it does not meet our submission requirements.\n`, + `**What failed:**`, + ...failures.map(f => `- ${f}`), + ``, + `**To submit an issue, please:**`, + `1. Use one of the [issue templates](https://github.com/${context.repo.owner}/${context.repo.repo}/issues/new/choose)`, + `2. Fill out the **AI Disclosure** checkboxes`, + `3. Copy the **Human Verification** phrase exactly as shown`, + `4. Read our [AI Policy](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/AI_POLICY.md)`, + ``, + `If you believe this was a mistake, please reach out on [Discord](https://discord.gg/9GEQrxmVyE).`, + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: comment, + }); + + // Best-effort labeling: prefer "spam", then fall back to labels + // that commonly exist on repos. + let labelApplied = false; + for (const label of ['spam', 'auto-closed', 'invalid']) { + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [label], + }); + labelApplied = true; + break; + } catch (e) { + console.log(`Label '${label}' unavailable or failed to apply, trying next`); + } + } + if (!labelApplied) { + console.log('No close label could be applied; continuing with close/lock'); + } + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed', + }); + + await github.rest.issues.lock({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + lock_reason: 'spam', + }); + + console.log(`Issue #${issue.number} closed and locked`); diff --git a/.github/workflows/letta-code-sync.yml b/.github/workflows/letta-code-sync.yml new file mode 100644 index 0000000..391047b --- /dev/null +++ b/.github/workflows/letta-code-sync.yml @@ -0,0 +1,19 @@ +name: Sync Code + +on: + push: + branches: + - main + +jobs: + notify: + runs-on: ubuntu-latest + if: ${{ !contains(github.event.head_commit.message, '[sync-skip]') }} + steps: + - name: Trigger repository_dispatch + run: | + curl -X POST \ + -H "Authorization: token ${{ secrets.SYNC_PAT }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/letta-ai/letta-cloud/dispatches \ + -d '{"event_type":"oss-update"}' diff --git a/.github/workflows/lint-command.yml b/.github/workflows/lint-command.yml new file mode 100644 index 0000000..0b3acfa --- /dev/null +++ b/.github/workflows/lint-command.yml @@ -0,0 +1,160 @@ +name: Lint Command + +on: + issue_comment: + types: [created] + + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to run lint on' + required: true + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + lint-command: + name: Handle /lint command + runs-on: ubuntu-latest + if: | + (github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number) || + (github.event_name == 'issue_comment' && + github.event.issue.pull_request && + contains(github.event.comment.body, '/lint') && + startsWith(github.event.comment.body, '/lint')) + + steps: + - name: Add acknowledgment reaction + if: github.event_name == 'issue_comment' + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ github.event.comment.id }} + reactions: eyes + + - name: Check permissions + if: github.event_name == 'issue_comment' + uses: actions/github-script@v7 + with: + script: | + const { data: collaborator } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.actor + }); + + if (!['admin', 'write'].includes(collaborator.permission)) { + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: '❌ You need write permissions to run lint commands.' + }); + core.setFailed('Insufficient permissions'); + } + + - name: Get PR information + id: pr + uses: actions/github-script@v7 + with: + script: | + const pr_number = context.eventName === 'issue_comment' + ? context.issue.number + : ${{ github.event.inputs.pr_number || 'null' }}; + + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr_number + }); + + core.setOutput('branch', pr.head.ref); + core.setOutput('repo', pr.head.repo.full_name); + core.setOutput('sha', pr.head.sha); + core.setOutput('number', pr_number); + + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ steps.pr.outputs.branch }} + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Set up python 3.12 + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: uv sync --extra dev --extra postgres --extra external-tools + working-directory: . + +# - name: Run ruff check with fixes +# run: uv run ruff check --fix . +# +# - name: Run ruff format +# run: uv run ruff format . + + - name: Run isort, black, autoflake + run: uv run isort . --profile black && uv run black . && uv run autoflake --remove-all-unused-imports --remove-unused-variables --in-place --recursive --ignore-init-module-imports . + working-directory: . + + + - name: Check for changes + id: changes + run: | + if [[ -n $(git status --porcelain) ]]; then + echo "changes=true" >> $GITHUB_OUTPUT + else + echo "changes=false" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: steps.changes.outputs.changes == 'true' + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add . + git commit -m "style: lint / fmt + + Triggered by /lint command from @${{ github.actor }}" + git push + + - name: Comment on success + if: steps.changes.outputs.changes == 'true' + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ steps.pr.outputs.number }} + body: | + ✅ **Lint fixes applied successfully!** + + Ruff has automatically fixed linting issues and formatted the code. + Changes have been committed to the PR branch. + + - name: Comment on no changes + if: steps.changes.outputs.changes == 'false' + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ steps.pr.outputs.number }} + body: | + ✅ **No lint issues found!** + + The code is already properly formatted and passes all linting checks. + + - name: Comment on failure + if: failure() + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ steps.pr.outputs.number }} + body: | + ❌ **Lint command failed!** + + There was an error while running the lint fixes. Please check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. diff --git a/.github/workflows/manually_clear_old_issues.yml b/.github/workflows/manually_clear_old_issues.yml new file mode 100644 index 0000000..74f7734 --- /dev/null +++ b/.github/workflows/manually_clear_old_issues.yml @@ -0,0 +1,25 @@ +name: Clear Old Issues +on: + workflow_dispatch: + +jobs: + cleanup-old-issues: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v5 + with: + days-before-issue-stale: 60 + days-before-issue-close: 0 + stale-issue-label: "auto-closed" + stale-issue-message: "" + close-issue-message: "This issue has been automatically closed due to 60 days of inactivity." + days-before-pr-stale: -1 + days-before-pr-close: -1 + exempt-issue-labels: "" + only-issue-labels: "" + remove-stale-when-updated: true + operations-per-run: 1000 + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/migration-test.yml b/.github/workflows/migration-test.yml new file mode 100644 index 0000000..7b06110 --- /dev/null +++ b/.github/workflows/migration-test.yml @@ -0,0 +1,54 @@ +name: Alembic Migration Tester +on: + pull_request: + paths: + - '**.py' + workflow_dispatch: +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 15 + services: + postgres: + image: pgvector/pgvector:pg17 + ports: + - 5432:5432 + env: + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + - run: psql -h localhost -U postgres -d postgres -c 'CREATE EXTENSION vector' + + - name: Set up python 3.11 + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: 3.11 + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install Dependencies + run: | + uv sync --all-extras + + - name: Test alembic migration + env: + LETTA_PG_PORT: 5432 + LETTA_PG_USER: postgres + LETTA_PG_PASSWORD: postgres + LETTA_PG_DB: postgres + LETTA_PG_HOST: localhost + run: | + uv run alembic upgrade head + uv run alembic check diff --git a/.github/workflows/model-sweep.yaml b/.github/workflows/model-sweep.yaml new file mode 100644 index 0000000..2475632 --- /dev/null +++ b/.github/workflows/model-sweep.yaml @@ -0,0 +1,142 @@ +name: Model Sweep +on: + workflow_dispatch: + inputs: + branch-name: + required: true + type: string + +jobs: + model-sweep: + runs-on: [self-hosted, medium] + services: + qdrant: + image: qdrant/qdrant + ports: + - 6333:6333 + postgres: + image: pgvector/pgvector:pg17 + ports: + - 5432:5432 + env: + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Check if gh is installed + run: | + if ! command -v gh >/dev/null 2>&1 + then + echo "gh could not be found, installing now..." + # install gh cli + (type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \ + && sudo mkdir -p -m 755 /etc/apt/keyrings \ + && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + && cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ + && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && sudo apt update \ + && sudo apt install gh -y + fi + + - name: Checkout + uses: actions/checkout@v4 + + - name: Inject env vars into environment + run: | + # Get secrets and mask them before adding to environment + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ -n "$line" ]]; then + value=$(echo "$line" | cut -d= -f2-) + echo "::add-mask::$value" + echo "$line" >> $GITHUB_ENV + fi + done < <(letta_secrets_helper --env dev --service ci) + + - name: Install dependencies + shell: bash + run: uv sync --extra dev --extra postgres --extra external-tools --extra cloud-tool-sandbox + - name: Migrate database + env: + LETTA_PG_PORT: 5432 + LETTA_PG_USER: postgres + LETTA_PG_PASSWORD: postgres + LETTA_PG_DB: postgres + LETTA_PG_HOST: localhost + run: | + psql -h localhost -U postgres -d postgres -c 'CREATE EXTENSION vector' + uv run alembic upgrade head + + - name: Run integration tests + # if any of the 1000+ test cases fail, pytest reports exit code 1 and won't procces/upload the results + continue-on-error: true + env: + LETTA_PG_PORT: 5432 + LETTA_PG_USER: postgres + LETTA_PG_PASSWORD: postgres + LETTA_PG_DB: postgres + LETTA_PG_HOST: localhost + LETTA_SERVER_PASS: test_server_token + OPENAI_API_KEY: ${{ env.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ env.ANTHROPIC_API_KEY }} + AZURE_API_KEY: ${{ env.AZURE_API_KEY }} + AZURE_BASE_URL: ${{ secrets.AZURE_BASE_URL }} + GEMINI_API_KEY: ${{ env.GEMINI_API_KEY }} + GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT}} + GOOGLE_CLOUD_LOCATION: ${{ secrets.GOOGLE_CLOUD_LOCATION}} + DEEPSEEK_API_KEY: ${{ env.DEEPSEEK_API_KEY}} + run: | + uv run pytest \ + -s -vv \ + .github/scripts/model-sweep/model_sweep.py \ + --json-report --json-report-file=.github/scripts/model-sweep/model_sweep_report.json --json-report-indent=4 + + - name: Convert report to markdown + continue-on-error: true + # file path args to generate_model_sweep_markdown.py are relative to the script + run: | + uv run python \ + .github/scripts/model-sweep/generate_model_sweep_markdown.py \ + .github/scripts/model-sweep/model_sweep_report.json \ + .github/scripts/model-sweep/supported-models.mdx + echo "Model sweep report saved to .github/scripts/model-sweep/supported-models.mdx" + + - id: date + run: echo "date=$(date +%Y-%m-%d)" >> $GITHUB_OUTPUT + + - name: commit and open pull request + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH_NAME=model-sweep/${{ inputs.branch-name || format('{0}', steps.date.outputs.date) }} + gh auth setup-git + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b $BRANCH_NAME + git add .github/scripts/model-sweep/supported-models.mdx + git commit -m "Update model sweep report" + # only push if changes were made + if git diff main --quiet; then + echo "No changes detected, skipping push" + exit 0 + else + git push origin $BRANCH_NAME + gh pr create \ + --base main \ + --head $BRANCH_NAME \ + --title "chore: update model sweep report" \ + --body "Automated PR to update model sweep report" + fi + + - name: Upload model sweep report + if: always() + uses: actions/upload-artifact@v4 + with: + name: model-sweep-report + path: .github/scripts/model-sweep/model_sweep_report.json diff --git a/.github/workflows/notify-on-update.yaml b/.github/workflows/notify-on-update.yaml new file mode 100644 index 0000000..3e043e3 --- /dev/null +++ b/.github/workflows/notify-on-update.yaml @@ -0,0 +1,29 @@ +name: Notify Submodule Repos +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Generate GitHub App Token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.NOTIFIER_APP_ID }} + private-key: ${{ secrets.NOTIFIER_PRIVATE_KEY }} + repositories: letta-cloud + + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v3.0.0 + with: + token: ${{ steps.app-token.outputs.token }} + repository: letta-ai/letta-cloud + event-type: letta-main-updated + client-payload: | + { + "commit_sha": "${{ github.sha }}", + "ref": "${{ github.ref }}" + } \ No newline at end of file diff --git a/.github/workflows/poetry-publish-nightly.yml b/.github/workflows/poetry-publish-nightly.yml new file mode 100644 index 0000000..8c8979f --- /dev/null +++ b/.github/workflows/poetry-publish-nightly.yml @@ -0,0 +1,64 @@ +name: uv-publish-nightly +on: + schedule: + - cron: '35 10 * * *' # 10:35am UTC, 2:35am PST, 5:35am EST + release: + types: [published] + workflow_dispatch: + +jobs: + # nightly release check from https://stackoverflow.com/a/67527144 + check-date: + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.should_run.outputs.should_run }} + steps: + - uses: actions/checkout@v4 + - name: print latest_commit + run: echo ${{ github.sha }} + - id: should_run + continue-on-error: true + name: check latest commit is less than a day + if: ${{ github.event_name == 'schedule' }} + run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false" + + build-and-publish-nightly: + name: Build and Publish to PyPI (nightly) + if: github.repository == 'letta-ai/letta' # TODO: if the repo org ever changes, this must be updated + runs-on: ubuntu-latest + needs: check-date + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Set up python 3.12 + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Set release version + run: | + # Extract the version number from pyproject.toml using awk + CURRENT_VERSION=$(awk -F '"' '/version =/ { print $2 }' pyproject.toml | head -n 1) + # Export the CURRENT_VERSION with the .dev and current date suffix + NIGHTLY_VERSION="${CURRENT_VERSION}.dev$(date +%Y%m%d%H%M%S)" + # Overwrite pyproject.toml with nightly config + sed -i "0,/version = \"${CURRENT_VERSION}\"/s//version = \"${NIGHTLY_VERSION}\"/" pyproject.toml + sed -i 's/name = "letta"/name = "letta-nightly"/g' pyproject.toml + sed -i "s/__version__ = '.*'/__version__ = '${NIGHTLY_VERSION}'/g" letta/__init__.py + cat pyproject.toml + cat letta/__init__.py + + - name: Build the Python package + run: uv build + + - name: Publish the package to PyPI + env: + UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }} + run: uv publish diff --git a/.github/workflows/poetry-publish.yml b/.github/workflows/poetry-publish.yml new file mode 100644 index 0000000..8441a73 --- /dev/null +++ b/.github/workflows/poetry-publish.yml @@ -0,0 +1,33 @@ +name: uv-publish +on: + release: + types: [published] + workflow_dispatch: + +jobs: + build-and-publish: + name: Build and Publish to PyPI + if: github.repository == 'letta-ai/letta' # TODO: if the repo org ever changes, this must be updated + runs-on: ubuntu-latest + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Set up python 3.12 + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Build the Python package + run: uv build + + - name: Publish the package to PyPI + env: + UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }} + run: uv publish diff --git a/.github/workflows/reusable-test-workflow.yml b/.github/workflows/reusable-test-workflow.yml new file mode 100644 index 0000000..955cd72 --- /dev/null +++ b/.github/workflows/reusable-test-workflow.yml @@ -0,0 +1,480 @@ +name: Reusable Test Workflow + +on: + workflow_call: + inputs: + test-type: + description: 'Type of tests to run (unit, integration, docker, send-message, sqlite)' + required: true + type: string + core-directory: + description: 'Working directory for commands. Uses . (root) by default.' + required: false + type: string + default: '.' + install-args: + description: 'uv sync arguments' + required: true + type: string + test-command: + description: 'Command to run tests' + required: false + type: string + default: 'uv run --frozen pytest -svv' + test-path-prefix: + description: 'Prefix for test path (e.g., tests/)' + required: false + type: string + default: 'tests/' + timeout-minutes: + description: 'Timeout in minutes' + required: false + type: number + default: 15 + runner: + description: 'Runner to use' + required: false + type: string + default: '["self-hosted", "small"]' + matrix-strategy: + description: 'JSON string for matrix strategy' + required: false + type: string + default: '{}' + changed-files-pattern: + description: 'Pattern for changed files detection' + required: false + type: string + default: | + ** + .github/workflows/reusable-test-workflow.yml + skip-fern-generation: + description: 'Skip Fern SDK generation' + required: false + type: boolean + default: false + use-docker: + description: 'Use Docker for tests' + required: false + type: boolean + default: false + ref: + description: 'Git ref to wait for checks on' + required: false + type: string + default: ${{ github.sha }} + use-redis: + description: 'Use Redis for tests' + required: false + type: boolean + default: false + is-external-pr: + description: 'Whether this is an external PR that needs protection' + required: false + type: boolean + default: false + +jobs: + changed-files: + runs-on: ${{ fromJSON(inputs.runner) }} + name: changed-files + outputs: + all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }} + any_changed: ${{ steps.changed-files.outputs.any_changed }} + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v46.0.4 + with: + files: ${{ inputs.changed-files-pattern }} + + cache-check: + needs: [changed-files] + runs-on: ${{ fromJSON(inputs.runner) }} + name: Check cache key + outputs: + cache_key: ${{ steps.cache-key.outputs.key }} + cache_hit: ${{ steps.cache.outputs.cache-hit }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + + - name: Generate cache key + if: inputs.skip-fern-generation != true || (!contains(needs.changed-files.outputs.all_changed_files, 'fern/openapi.json') && !contains(needs.changed-files.outputs.all_changed_files, 'fern/openapi-overrides.yml')) + id: cache-key + run: | + echo "key=sdk-${{ github.ref_name }}-${{ hashFiles('fern/*', 'pyproject.toml') }}" >> $GITHUB_OUTPUT + + - name: Restore SDK cache + # skip if "skip-fern-generation" is true or if the upstream workflow would've generated an sdk preview (changes to openapi files) + if: inputs.skip-fern-generation != true || (!contains(needs.changed-files.outputs.all_changed_files, 'fern/openapi.json') && !contains(needs.changed-files.outputs.all_changed_files, 'fern/openapi-overrides.yml')) + id: cache + uses: actions/cache/restore@v4 + with: + path: | + fern/.preview/fern-python-sdk/ + key: ${{ steps.cache-key.outputs.key }} + fail-on-cache-miss: false + + block-until-sdk-preview-finishes: + needs: [changed-files, cache-check] + if: | + needs.cache-check.outputs.cache_hit != 'true' + timeout-minutes: ${{ inputs.timeout-minutes }} + runs-on: ${{ fromJSON(inputs.runner) }} + name: block-until-sdk-preview-finishes + steps: + - name: Debug ref information + run: | + echo "Input ref: ${{ inputs.ref }}" + echo "GitHub SHA: ${{ github.sha }}" + echo "GitHub ref: ${{ github.ref }}" + echo "PR head SHA: ${{ github.event.pull_request.head.sha }}" + echo "Event name: ${{ github.event_name }}" + + - name: Wait for Preview SDK workflow + if: inputs.skip-fern-generation != true || (!contains(needs.changed-files.outputs.all_changed_files, 'fern/openapi.json') && !contains(needs.changed-files.outputs.all_changed_files, 'fern/openapi-overrides.yml')) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "Waiting for 'preview-python-sdk' check to complete on ref: ${{ inputs.ref }}" + + # Wait for the check to complete with timeout + timeout_seconds=1800 + interval_seconds=60 + elapsed=0 + + while [ $elapsed -lt $timeout_seconds ]; do + echo "Checking status... (elapsed: ${elapsed}s)" + + # Get check runs using pr checks syntax with branch name or PR number + if [ "${{ github.event_name }}" = "pull_request" ]; then + pr_identifier="${{ github.event.pull_request.number }}" + else + pr_identifier="${{ github.ref_name }}" + fi + + check_info=$(gh pr checks "$pr_identifier" -R ${{ github.repository }} --json name,state,startedAt \ + | jq -r '.[] | select(.name == "preview-python-sdk") | [.startedAt, .state] | @tsv' | sort -r | head -1 | cut -f2) + + if [ -n "$check_info" ]; then + echo "Check state: $check_info" + + if [ "$check_info" = "SUCCESS" ] || [ "$check_info" = "SKIPPED" ]; then + echo "Check completed with state: $check_info" + exit 0 + elif [ "$check_info" = "FAILURE" ] || [ "$check_info" = "CANCELLED" ]; then + echo "❌ Preview Python SDK build failed with state: $check_info" + echo "🚫 Blocking dependent test jobs to prevent extraneous failures" + echo "📋 To fix: Check the 'preview-python-sdk' job logs for build errors" + exit 1 + fi + else + echo "Check 'preview-python-sdk' not found yet" + fi + + sleep $interval_seconds + elapsed=$((elapsed + interval_seconds)) + done + + echo "Timeout waiting for check to complete" + exit 1 + + test-run: + needs: [changed-files, block-until-sdk-preview-finishes] + if: | + always() && + needs.changed-files.outputs.any_changed == 'true' && + (needs.block-until-sdk-preview-finishes.result == 'success' || + needs.block-until-sdk-preview-finishes.result == 'skipped') + + runs-on: ${{ fromJSON(inputs.runner) }} + timeout-minutes: ${{ inputs.timeout-minutes }} + strategy: ${{ fromJSON(inputs.matrix-strategy) }} + + services: + postgres: + image: pgvector/pgvector:pg17 + ports: + # avoids conflict with docker postgres + - ${{ inputs.use-docker && '9999:5432' || '5432:5432' }} + env: + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + redis: + image: ${{ inputs.use-redis && 'redis:8-alpine' || '' }} + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Set core directory + id: detect-core-dir + run: | + echo "dir=${{ inputs.core-directory }}" >> $GITHUB_OUTPUT + echo "detected=manual" >> $GITHUB_OUTPUT + echo "Using core directory: $(cat $GITHUB_OUTPUT | grep '^dir=' | cut -d'=' -f2)" + + - name: Generate cache key + if: inputs.skip-fern-generation != true || (!contains(needs.changed-files.outputs.all_changed_files, 'fern/openapi.json') && !contains(needs.changed-files.outputs.all_changed_files, 'fern/openapi-overrides.yml')) + id: cache-key + run: | + echo "key=sdk-${{ github.ref_name }}-${{ hashFiles('fern/*', 'pyproject.toml') }}" >> $GITHUB_OUTPUT + + - name: Restore SDK cache + # skip if "skip-fern-generation" is true or if the upstream workflow would've generated an sdk preview (changes to openapi files) + if: inputs.skip-fern-generation != true || (!contains(needs.changed-files.outputs.all_changed_files, 'fern/openapi.json') && !contains(needs.changed-files.outputs.all_changed_files, 'fern/openapi-overrides.yml')) + id: restore-sdk-cache + uses: actions/cache/restore@v4 + with: + path: | + fern/.preview/fern-python-sdk/ + key: ${{ steps.cache-key.outputs.key }} + fail-on-cache-miss: false + + - name: Check SDK cache availability + if: (inputs.skip-fern-generation != true || (!contains(needs.changed-files.outputs.all_changed_files, 'fern/openapi.json') && !contains(needs.changed-files.outputs.all_changed_files, 'fern/openapi-overrides.yml'))) && steps.restore-sdk-cache.outputs.cache-hit != 'true' + run: | + echo "❌ Preview Python SDK cache expired or missing!" + echo "📦 Cache key: ${{ steps.cache-key.outputs.key }}" + echo "🔄 To fix: Re-run the 'preview-python-sdk' workflow job to regenerate the SDK" + echo "💡 This can happen when:" + echo " - The cache entry has expired" + echo " - Dependencies in fern/* or pyproject.toml have changed" + echo " - The preview-python-sdk job hasn't run successfully for this branch/commit" + exit 1 + + - name: Install dependencies with retry + shell: bash + working-directory: . + run: | + uv sync --no-install-project ${{ inputs.install-args }} + + - name: Install custom SDK + if: inputs.skip-fern-generation != true + working-directory: . + run: | + echo "Fixing Fern SDK pyproject.toml for uv compatibility..." + SDK_PYPROJECT="fern/.preview/fern-python-sdk/pyproject.toml" + VERSION=$(grep -A 10 '^\[tool\.poetry\]' "$SDK_PYPROJECT" | grep '^version' | head -1 | cut -d'"' -f2) + head -n 2 < fern/.preview/fern-python-sdk/pyproject.toml > fern/.preview/fern-python-sdk/pyproject.toml.tmp + echo "version = \"$VERSION\"" >> fern/.preview/fern-python-sdk/pyproject.toml.tmp + tail -n +3 fern/.preview/fern-python-sdk/pyproject.toml >> fern/.preview/fern-python-sdk/pyproject.toml.tmp + mv fern/.preview/fern-python-sdk/pyproject.toml.tmp fern/.preview/fern-python-sdk/pyproject.toml + + uv pip install -e fern/.preview/fern-python-sdk/. + - name: Migrate database + if: inputs.use-docker != true && inputs.test-type != 'sqlite' + working-directory: . + env: + LETTA_PG_PORT: 5432 + LETTA_PG_USER: postgres + LETTA_PG_PASSWORD: postgres + LETTA_PG_DB: postgres + LETTA_PG_HOST: localhost + run: | + psql -h localhost -U postgres -d postgres -c 'CREATE EXTENSION vector' + uv run alembic upgrade head + - name: Inject env vars into environment + if: inputs.is-external-pr != true + working-directory: . + run: | + # Get secrets and mask them before adding to environment + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ -n "$line" ]]; then + value=$(echo "$line" | cut -d= -f2-) + echo "::add-mask::$value" + echo "$line" >> $GITHUB_ENV + fi + done < <(letta_secrets_helper --env dev --service ci) + + - name: Docker setup for Docker tests + if: inputs.use-docker + run: | + mkdir -p /home/ci-runner/.letta/logs + sudo chown -R $USER:$USER /home/ci-runner/.letta/logs + chmod -R 755 /home/ci-runner/.letta/logs + + - name: Build and run docker dev server + if: inputs.use-docker && inputs.is-external-pr != true + env: + LETTA_PG_DB: letta + LETTA_PG_USER: letta + LETTA_PG_PASSWORD: letta + LETTA_PG_PORT: 5432 + OPENAI_API_KEY: ${{ env.OPENAI_API_KEY }} + run: | + cd libs/config-core-deploy + docker compose -f compose.yaml up --build -d + + - name: Wait for Docker service + if: inputs.use-docker + working-directory: ${{ steps.detect-core-dir.outputs.dir }} + run: | + bash scripts/wait_for_service.sh localhost:8083 -- echo "Service is ready" + + - name: Run tests + if: inputs.is-external-pr != true + working-directory: ${{ steps.detect-core-dir.outputs.dir }} + env: + # Database configuration (shared, but values depend on Docker usage) + LETTA_PG_PORT: 5432 + LETTA_PG_USER: ${{ inputs.use-docker && 'letta' || 'postgres' }} + LETTA_PG_PASSWORD: ${{ inputs.use-docker && 'letta' || 'postgres' }} + LETTA_PG_DB: ${{ inputs.use-docker && 'letta' || 'postgres' }} + LETTA_PG_HOST: localhost + + # Server configuration (conditional) + LETTA_SERVER_PASS: test_server_token + + # LLM Provider API Keys (shared across all test types) + OPENAI_API_KEY: ${{ env.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ env.ANTHROPIC_API_KEY }} + GEMINI_API_KEY: ${{ env.GEMINI_API_KEY }} + GROQ_API_KEY: ${{ env.GROQ_API_KEY }} + AZURE_API_KEY: ${{ env.AZURE_API_KEY }} + AZURE_BASE_URL: ${{ secrets.AZURE_BASE_URL }} + DEEPSEEK_API_KEY: ${{ env.DEEPSEEK_API_KEY }} + LETTA_MISTRAL_API_KEY: ${{ secrets.LETTA_MISTRAL_API_KEY }} + + # External service API Keys (shared across all test types) + E2B_API_KEY: ${{ env.E2B_API_KEY }} + E2B_SANDBOX_TEMPLATE_ID: ${{ env.E2B_SANDBOX_TEMPLATE_ID }} + + # Turbopuffer flags + LETTA_USE_TPUF: true + LETTA_TPUF_API_KEY: ${{ env.LETTA_TPUF_API_KEY }} + + # Encryption key + LETTA_ENCRYPTION_KEY: ${{ env.LETTA_ENCRYPTION_KEY }} + + # Google Cloud (shared across all test types) + GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }} + GOOGLE_CLOUD_LOCATION: ${{ secrets.GOOGLE_CLOUD_LOCATION }} + + # Real object store (required for git-backed memory integration test) + # Use DEV bucket/prefix variable to avoid prod resources. + LETTA_OBJECT_STORE_URI: ${{ vars.LETTA_OBJECT_STORE_URI_DEV }} + + # Feature flags (shared across all test types) + LETTA_ENABLE_BATCH_JOB_POLLING: true + + # Gemini flags + GEMINI_FORCE_MINIMUM_THINKING_BUDGET: true + GEMINI_MAX_RETRIES: 10 + + # Pinecone flags + LETTA_PINECONE_API_KEY: ${{ secrets.LETTA_PINECONE_API_KEY }} + LETTA_ENABLE_PINECONE: true + + EXA_API_KEY: ${{ env.EXA_API_KEY }} + + # Docker-specific environment variables + PYTHONPATH: ${{ inputs.use-docker && format('{0}:{1}', github.workspace, env.PYTHONPATH) || '' }} + + LETTA_REDIS_HOST: localhost + run: | + set -o xtrace + + # Set LETTA_SERVER_URL only for Docker tests + if [[ "${{ inputs.use-docker }}" == "true" ]]; then + export LETTA_SERVER_URL="http://localhost:8083" + fi + + # Set LLM_CONFIG_FILE only for send-message tests + if [[ "${{ inputs.test-type }}" == "send-message" ]]; then + export LLM_CONFIG_FILE="${{ matrix.config_file }}" + fi + + # Set Ollama base URL only for Ollama tests + if [[ "${{ inputs.test-type }}" == "integration" && "${{ inputs.runner }}" == *"ollama"* ]]; then + export LLM_CONFIG_FILE="ollama.json" + export OLLAMA_BASE_URL="http://localhost:11434" + fi + + # Set LMStudio base URL only for LMStudio tests + if [[ "${{ inputs.test-type }}" == "integration" && "${{ inputs.runner }}" == *"lmstudio"* ]]; then + export LLM_CONFIG_FILE="lmstudio.json" + export LMSTUDIO_BASE_URL="http://localhost:1234" + fi + + # Set VLLM base URL only for VLLM tests + if [[ "${{ inputs.test-type }}" == "integration" && "${{ inputs.runner }}" == *"vllm"* ]]; then + export LLM_CONFIG_FILE="vllm.json" + export VLLM_BASE_URL="http://localhost:8000" + fi + + uv pip install pytest-github-actions-annotate-failures + + # Handle different matrix variable names and test commands based on test type + if [[ "${{ inputs.test-type }}" == "integration" ]]; then + uv pip install letta + uv pip show letta + uv pip show letta-client + uv run --frozen pytest -svv ${{ inputs.test-path-prefix }}${{ matrix.test_suite }} + elif [[ "${{ inputs.test-type }}" == "unit" ]]; then + uv pip show letta-client + uv run --frozen pytest -svv ${{ inputs.test-path-prefix }}${{ matrix.test_suite }} + elif [[ "${{ inputs.test-type }}" == "send-message" ]]; then + uv run --frozen pytest -s -vv tests/integration_test_send_message.py --maxfail=1 --durations=10 + elif [[ "${{ inputs.test-type }}" == "docker" ]]; then + uv run --frozen pytest -s tests/test_client.py + elif [[ "${{ inputs.test-type }}" == "sqlite" ]]; then + # force sqlite + unset LETTA_PG_USER + unset LETTA_PG_PASSWORD + unset LETTA_PG_DB + unset LETTA_PG_HOST + uv pip show letta-client + uv run alembic upgrade head + uv run --frozen pytest -svv ${{ inputs.test-path-prefix }}${{ matrix.test_suite }} + else + ${{ inputs.test-command }} + fi + + - name: Remove sqlite db + if: ${{ always() && inputs.test-type == 'sqlite' }} + run: sudo rm -rf ~/.letta || true + + - name: Print docker logs if tests fail + if: ${{ (failure() || cancelled()) && inputs.use-docker }} + working-directory: libs/config-core-deploy + run: | + echo "Printing Docker Logs..." + docker compose -f compose.yaml logs + + - name: Stop docker + if: ${{ always() && inputs.use-docker }} + working-directory: libs/config-core-deploy + run: | + docker compose -f compose.yaml down --volumes + sudo rm -rf .persist diff --git a/.github/workflows/send-message-integration-tests.yml b/.github/workflows/send-message-integration-tests.yml new file mode 100644 index 0000000..d60a75e --- /dev/null +++ b/.github/workflows/send-message-integration-tests.yml @@ -0,0 +1,47 @@ +name: 🐍🧪 [Core] Send Message SDK Tests + +on: + pull_request: + branches: + - main + pull_request_target: + branches: + - main + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + send-message-tests: + # Run on pull_request OR on pull_request_target only when labeled "safe to test" + if: github.event_name == 'pull_request' || (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe to test')) + uses: ./.github/workflows/reusable-test-workflow.yml + with: + test-type: 'send-message' + is-external-pr: ${{ github.event_name == 'pull_request_target' && !contains(github.event.pull_request.labels.*.name, 'safe to test') }} + changed-files-pattern: | + ** + .github/workflows/reusable-test-workflow.yml + .github/workflows/send-message-integration-tests.yml + install-args: '--extra dev --extra postgres --extra external-tools --extra cloud-tool-sandbox --extra redis' + timeout-minutes: 15 + runner: '["self-hosted", "medium"]' + ref: ${{ github.event.pull_request.head.sha || github.sha }} + use-redis: true + # TODO: "azure-gpt-4o-mini.json" add back later, getting content violation + matrix-strategy: | + { + "fail-fast": false, + "matrix": { + "config_file": [ + "openai-gpt-4o-mini.json", + "openai-gpt-4.1.json", + "openai-gpt-5.json", + "claude-4-5-sonnet.json", + "gemini-2.5-pro.json", + ] + } + } + secrets: inherit diff --git a/.github/workflows/test-lmstudio.yml b/.github/workflows/test-lmstudio.yml new file mode 100644 index 0000000..19893c8 --- /dev/null +++ b/.github/workflows/test-lmstudio.yml @@ -0,0 +1,48 @@ +name: Self-Hosted Provider Integration - LMStudio + +on: + workflow_dispatch: + # inputs: + # ref: + # description: 'Git ref to test' + # required: false + # type: string + # default: ${{ github.sha || github.ref || github.event.pull_request.head.sha }} + pull_request: + paths: + - '**' + - '.github/workflows/test-lmstudio.yml' + - '.github/workflows/reusable-test-workflow.yml' + pull_request_target: + types: [labeled] + paths: + - '**' + - '.github/workflows/test-lmstudio.yml' + - '.github/workflows/reusable-test-workflow.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + test-lmstudio: + # Run on pull_request OR on pull_request_target only when labeled "safe to test" + if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe to test')) + uses: ./.github/workflows/reusable-test-workflow.yml + with: + test-type: "integration" + is-external-pr: ${{ github.event_name == 'pull_request_target' && !contains(github.event.pull_request.labels.*.name, 'safe to test') }} + install-args: "--extra postgres --extra external-tools --extra dev --extra cloud-tool-sandbox" + test-command: "uv run pytest -svv tests/" + timeout-minutes: 60 + runner: '["self-hosted", "gpu", "lmstudio"]' + matrix-strategy: | + { + "fail-fast": false, + "matrix": { + "test_suite": [ + "integration_test_send_message.py" + ] + } + } + secrets: inherit diff --git a/.github/workflows/test-ollama.yml b/.github/workflows/test-ollama.yml new file mode 100644 index 0000000..e5287d0 --- /dev/null +++ b/.github/workflows/test-ollama.yml @@ -0,0 +1,49 @@ +name: Self-Hosted Provider Integration - Ollama + +on: + workflow_dispatch: + # inputs: + # ref: + # description: 'Git ref to test' + # required: false + # type: string + # default: ${{ github.sha || github.ref || github.event.pull_request.head.sha }} + pull_request: + paths: + - '**' + - '.github/workflows/test-ollama.yml' + - '.github/workflows/reusable-test-workflow.yml' + pull_request_target: + types: [labeled] + paths: + - '**' + - '.github/workflows/test-ollama.yml' + - '.github/workflows/reusable-test-workflow.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + test-ollama: + # Run on pull_request OR on pull_request_target only when labeled "safe to test" + if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe to test')) + uses: ./.github/workflows/reusable-test-workflow.yml + with: + test-type: "integration" + is-external-pr: ${{ github.event_name == 'pull_request_target' && !contains(github.event.pull_request.labels.*.name, 'safe to test') }} + install-args: "--extra postgres --extra external-tools --extra dev --extra cloud-tool-sandbox" + test-command: "uv run --frozen pytest -svv tests/" + timeout-minutes: 60 + runner: '["self-hosted", "gpu", "ollama"]' + matrix-strategy: | + { + "fail-fast": false, + "matrix": { + "test_suite": [ + "test_providers.py::test_ollama", + "integration_test_send_message.py" + ] + } + } + secrets: inherit diff --git a/.github/workflows/test-pip-install.yml b/.github/workflows/test-pip-install.yml new file mode 100644 index 0000000..c01c93e --- /dev/null +++ b/.github/workflows/test-pip-install.yml @@ -0,0 +1,23 @@ +name: Test Package Installation + +on: [push, pull_request, workflow_dispatch] + +jobs: + test-install: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12", "3.13"] # Adjust Python versions as needed + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package with extras + run: pip install '.[external-tools,postgres,dev,server,ollama]' # Replace 'all' with the key that includes all extras + + - name: Check package installation + run: pip list # Or any other command to verify successful installation diff --git a/.github/workflows/test-vllm.yml b/.github/workflows/test-vllm.yml new file mode 100644 index 0000000..65b9ba0 --- /dev/null +++ b/.github/workflows/test-vllm.yml @@ -0,0 +1,45 @@ +name: Self-Hosted Provider Integration - vLLM + +on: + workflow_dispatch: + # inputs: + # ref: + # description: 'Git ref to test' + # required: false + # type: string + # default: ${{ github.sha || github.ref || github.event.pull_request.head.sha }} + pull_request: + paths: + - '**' + - '.github/workflows/test-vllm.yml' + - '.github/workflows/reusable-test-workflow.yml' + pull_request_target: + types: [labeled] + paths: + - '**' + - '.github/workflows/test-vllm.yml' + - '.github/workflows/reusable-test-workflow.yml' + +jobs: + test-vllm: + # Run on pull_request OR on pull_request_target only when labeled "safe to test" + if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe to test')) + uses: ./.github/workflows/reusable-test-workflow.yml + with: + test-type: "integration" + is-external-pr: ${{ github.event_name == 'pull_request_target' && !contains(github.event.pull_request.labels.*.name, 'safe to test') }} + install-args: "--extra postgres --extra external-tools --extra dev --extra cloud-tool-sandbox" + test-command: "uv run --frozen pytest -svv tests/" + timeout-minutes: 60 + runner: '["self-hosted", "gpu", "vllm"]' + matrix-strategy: | + { + "fail-fast": false, + "matrix": { + "test_suite": [ + "test_providers.py::test_vllm", + "integration_test_send_message.py" + ] + } + } + secrets: inherit diff --git a/.github/workflows/warn_poetry_updates.yml b/.github/workflows/warn_poetry_updates.yml new file mode 100644 index 0000000..f5761ac --- /dev/null +++ b/.github/workflows/warn_poetry_updates.yml @@ -0,0 +1,63 @@ +name: Check uv Dependencies Changes + +on: + pull_request: + paths: + - 'uv.lock' + - 'pyproject.toml' + +jobs: + check-uv-changes: + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for uv.lock changes + id: check-uv-lock + run: | + if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | grep -q "uv.lock"; then + echo "uv_lock_changed=true" >> $GITHUB_OUTPUT + else + echo "uv_lock_changed=false" >> $GITHUB_OUTPUT + fi + + - name: Check for pyproject.toml changes + id: check-pyproject + run: | + if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | grep -q "pyproject.toml"; then + echo "pyproject_changed=true" >> $GITHUB_OUTPUT + else + echo "pyproject_changed=false" >> $GITHUB_OUTPUT + fi + + - name: Create PR comment + if: steps.check-uv-lock.outputs.uv_lock_changed == 'true' || steps.check-pyproject.outputs.pyproject_changed == 'true' + uses: actions/github-script@v7 + with: + script: | + const uvLockChanged = ${{ steps.check-uv-lock.outputs.uv_lock_changed }}; + const pyprojectChanged = ${{ steps.check-pyproject.outputs.pyproject_changed }}; + + let message = '📦 Dependencies Alert:\n\n'; + + if (uvLockChanged && pyprojectChanged) { + message += '- Both `uv.lock` and `pyproject.toml` have been modified\n'; + } else if (uvLockChanged) { + message += '- `uv.lock` has been modified\n'; + } else if (pyprojectChanged) { + message += '- `pyproject.toml` has been modified\n'; + } + + message += '\nPlease review these changes carefully to ensure they are intended (cc @sarahwooders @cpacker).'; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: message + }); diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea36037 --- /dev/null +++ b/.gitignore @@ -0,0 +1,1005 @@ +# Below are generated by gitignor.io (toptal) +# Created by https://www.toptal.com/developers/gitignore/api/vim,linux,macos,pydev,python,eclipse,pycharm,windows,netbeans,pycharm+all,pycharm+iml,visualstudio,jupyternotebooks,visualstudiocode,xcode,xcodeinjection +# Edit at https://www.toptal.com/developers/gitignore?templates=vim,linux,macos,pydev,python,eclipse,pycharm,windows,netbeans,pycharm+all,pycharm+iml,visualstudio,jupyternotebooks,visualstudiocode,xcode,xcodeinjection + +openapi_letta.json +openapi_openai.json + +CLAUDE.md + +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + +### Eclipse Patch ### +# Spring Boot Tooling +.sts4-cache/ + +### JupyterNotebooks ### +# gitignore template for Jupyter Notebooks +# website: http://jupyter.org/ + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Remove previous ipynb_checkpoints +# git rm -r .ipynb_checkpoints/ + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### NetBeans ### +**/nbproject/private/ +**/nbproject/Makefile-*.mk +**/nbproject/Package-*.bash +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### PyCharm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# AWS User-specific + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# SonarLint plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### PyCharm+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### PyCharm+iml ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# AWS User-specific + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# SonarLint plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### PyCharm+iml Patch ### +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +### pydev ### +.pydevproject + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +develop-eggs/ +downloads/ +eggs#letta/letta-server:0.3.7 +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook + +# IPython + +# pdm +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### VisualStudioCode ### +.vscode/ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Xcode ### +## User settings +xcuserdata/ + +## Xcode 8 and earlier +*.xcscmblueprint +*.xccheckout + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings + +### XcodeInjection ### +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp_proj +*_wpftmp.csproj +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +*.code-workspace + +# Local History for Visual Studio Code + +# Windows Installer files from build outputs + +# JetBrains Rider +*.sln.iml + +### VisualStudio Patch ### +# Additional files built by Visual Studio + +# End of https://www.toptal.com/developers/gitignore/api/vim,linux,macos,pydev,python,eclipse,pycharm,windows,netbeans,pycharm+all,pycharm+iml,visualstudio,jupyternotebooks,visualstudiocode,xcode,xcodeinjection + + +## cached db data +pgdata/ +!pgdata/.gitkeep +.persist/ + +## pytest mirrors +letta/.pytest_cache/ +memgpy/pytest.ini +**/**/pytest_cache + +## ignore venvs +tests/test_tool_sandbox/restaurant_management_system/venv + +## custom scripts +test diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..90fd016 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-yaml + exclude: 'docs/.*|tests/data/.*|configs/.*|helm/.*' + - id: end-of-file-fixer + exclude: 'docs/.*|tests/data/.*|letta/server/static_files/.*|.*/.*\.(scss|css|html)' + - id: trailing-whitespace + exclude: 'docs/.*|tests/data/.*|letta/server/static_files/.*' + + - repo: local + hooks: + - id: trufflehog + name: TruffleHog + entry: bash -c 'trufflehog git file://. --since-commit HEAD --results=verified,unknown --fail --no-update' + language: system + stages: ["pre-commit", "pre-push"] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.11 + hooks: + - id: ruff-check + args: [ --fix ] + - id: ruff-format + + - repo: local + hooks: + - id: ty + name: ty check + entry: uv run ty check . + language: python diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a8ce42d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,19 @@ +# AGENTS.md + +## This repository is deprecated + +This repository contains the **legacy Letta server**: the self-hosted API server (`letta/letta` image) that powers the Letta V1 API and V1 SDKs (`@letta-ai/letta-client` for TypeScript, `letta-client` for Python). It is in maintenance mode and is no longer where active development happens. + +If you are an agent working here, you most likely want one of these instead: + +| You want to... | Go to | +| --- | --- | +| Use or modify the current Letta agent (harness, CLI, channel integrations, etc.) | [letta-ai/letta-code](https://github.com/letta-ai/letta-code) | +| Build agents into an application programmatically | [Letta Agent SDK](https://docs.letta.com/letta-agent-sdk/overview) (`@letta-ai/letta-agent-sdk`) | +| Self-host a server for Letta agents | [App Server](https://docs.letta.com/letta-agent/app-server), which replaces the API server in this repo | +| Reference the legacy V1 SDK / API | [V1 SDK docs](https://docs.letta.com/guides/get-started/intro) | + +## Notes for agents working in this repo + +- New features and integrations should target [letta-ai/letta-code](https://github.com/letta-ai/letta-code) and the [Letta Agent SDK](https://docs.letta.com/letta-agent-sdk/overview), not this codebase. +- All contributions (issues, PRs, discussions) must comply with the [AI usage policy](AI_POLICY.md): AI assistance must be disclosed, and a human must fully understand and review everything submitted. diff --git a/AI_POLICY.md b/AI_POLICY.md new file mode 100644 index 0000000..088b8e9 --- /dev/null +++ b/AI_POLICY.md @@ -0,0 +1,63 @@ +# AI Usage Policy + +> This policy is adapted from [Ghostty's AI Policy](https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md) with modifications for the Letta project. + +## Rules + +- **All AI usage in any form must be disclosed.** You must state + the tool you used (e.g. Claude Code, Cursor, Copilot, ChatGPT) along with + the extent that the work was AI-assisted. + +- **The human-in-the-loop must fully understand all code.** If you + can't explain what your changes do and how they interact with the + greater system without the aid of AI tools, do not contribute + to this project. + +- **Issues and discussions can use AI assistance but must have a full + human-in-the-loop.** This means that any content generated with AI + must have been reviewed _and edited_ by a human before submission. + AI is very good at being overly verbose and including noise that + distracts from the main point. Humans must do their research and + trim this down. + +- **No AI-generated media is allowed (art, images, videos, audio, etc.).** + Text and code are the only acceptable AI-generated content, per the + other rules in this policy. + +## Enforcement + +Issues that do not comply with this policy will be **automatically closed and locked**. +Specifically, all issues must: + +1. Fill out the **AI Disclosure** checkboxes indicating whether the issue was human-written or AI-assisted. +2. Include the **Human Verification** phrase as instructed in the issue template. +3. Acknowledge that they have read this policy. + +Members of the [letta-ai](https://github.com/letta-ai) GitHub organization and +[trusted contributors](.github/TRUSTED_CONTRIBUTORS) are exempt from automated checks, +but are still expected to follow the spirit of this policy. + +## There are Humans Here + +Please remember that Letta is maintained by humans. + +Every discussion, issue, and pull request is read and reviewed by +humans. It is a boundary point at which people interact with each other +and the work done. It is rude and disrespectful to approach this boundary +with low-effort, unqualified work, since it puts the burden of +validation on the maintainer. + +## AI is Welcome Here + +Letta is a company that builds AI tools — of course we use AI! +Many of our maintainers use AI tools extensively in their daily workflow. +As a project, we welcome AI as a tool. + +**Our reason for the strict AI policy is not due to an anti-AI stance**, but +instead due to the volume of low-quality, AI-generated issues and PRs +that waste maintainer time. It's the quality of the contribution that +matters, not whether AI was involved in creating it. + +Maintainers are exempt from automated enforcement of these rules and +may use AI tools at their discretion; they've proven themselves +trustworthy to apply good judgment. diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..3dc6ada --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,25 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +title: "Letta" +url: "https://github.com/letta-ai/letta" +preferred-citation: + type: article + authors: + - family-names: "Packer" + given-names: "Charles" + - family-names: "Wooders" + given-names: "Sarah" + - family-names: "Lin" + given-names: "Kevin" + - family-names: "Fang" + given-names: "Vivian" + - family-names: "Patil" + given-names: "Shishir G" + - family-names: "Stoica" + given-names: "Ion" + - family-names: "Gonzalez" + given-names: "Joseph E" + journal: "arXiv preprint arXiv:2310.08560" + month: 10 + title: "MemGPT: Towards LLMs as Operating Systems" + year: 2023 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..750c29d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,183 @@ +# How to Contribute to Letta + +Thank you for investing time in contributing to our project! Here's a guide to get you started. + +## AI Policy + +**All contributions must comply with our [AI Usage Policy](AI_POLICY.md).** + +In short: AI tools are welcome, but you must disclose their use, and a human must fully understand and review all submitted work. Issues and PRs that appear to be unreviewed AI output will be closed. See the full policy for details. + +## 1. 🚀 Getting Started + +### 🍴 Fork the Repository + +First things first, let's get you a personal copy of Letta to play with. Think of it as your very own playground. 🎪 + +1. Head over to the Letta repository on GitHub. +2. In the upper-right corner, hit the 'Fork' button. + +### 🚀 Clone the Repository + +Now, let's bring your new playground to your local machine. + +```shell +git clone https://github.com/your-username/letta.git +``` + +### 🧩 Install dependencies & configure environment + +This project requires **PostgreSQL** to be installed and running on your system. Assuming you have a running PostgreSQL instance, first you need to create the user, database and ensure the pgvector +extension is ready. Here are sample steps for a case where user and database name is letta and assumes no password is set: + +#### 1. Enter the PostgreSQL Shell +Open your terminal (or Command Prompt on Windows) and run: +```bash +# On Mac/Linux: +sudo -u postgres psql + +# On Windows: +psql -U postgres + +``` +#### 2. Run Setup Commands +Once inside the PostgreSQL prompt (you will see `postgres=#`), run the following SQL block: + +```sql +-- 1. Create a dedicated role with login and superuser permissions +CREATE ROLE letta WITH LOGIN SUPERUSER PASSWORD 'letta'; + +-- 2. Create the database and assign 'letta' as the owner +CREATE DATABASE letta OWNER letta; + +-- 3. Switch connection to the new 'letta' database +\c letta + +-- 4. Enable the pgvector extension for vector embeddings +CREATE EXTENSION IF NOT EXISTS vector; + +Setup the environment variable to tell letta code to contact PostgreSQL database: +```shell +export LETTA_PG_URI="postgresql://${POSTGRES_USER:-letta}:${POSTGRES_PASSWORD:-letta}@localhost:5432/${POSTGRES_DB:-letta}" +``` + +#### Install uv and dependencies + +First, install uv using [the official instructions here](https://docs.astral.sh/uv/getting-started/installation/). + +Once uv is installed, navigate to the letta directory and install the Letta project with uv: +```shell +cd letta +eval $(uv env activate) +uv sync --all-extras +``` +``` +After this you need to prep the database with initial content. You can use alembic upgrade to populate the initial +contents from template test data. +```shell +uv run alembic upgrade head +``` + +#### Running letta with uv + +Now when you want to use `letta`, you can use `uv run` to run any letta command: +```shell +uv run letta server +``` + +#### Installing pre-commit +We recommend installing pre-commit to ensure proper formatting during development: +``` +uv run pre-commit install +uv run pre-commit run --all-files +``` +If you don't install pre-commit, you will need to run `uv run black .` before submitting a PR. + +## 2. 🛠️ Making Changes + +### 🌟 Create a Branch + +Time to put on your creative hat and make some magic happen. First, let's create a new branch for your awesome changes. 🧙‍♂️ + +```shell +git checkout -b feature/your-feature +``` + +### ✏️ Make your Changes + +Now, the world is your oyster! Go ahead and craft your fabulous changes. 🎨 + + +#### Handling Database Migrations +If you are running Letta for the first time, your database will be automatically be setup. If you are updating Letta, you may need to run migrations. To run migrations, use the following command: +```shell +uv run alembic upgrade head +``` + +#### Creating a new Database Migration +If you have made changes to the database models, you will need to create a new migration. To create a new migration, use the following command: +```shell +uv run alembic revision --autogenerate -m "Your migration message here" +``` + +Visit the [Alembic documentation](https://alembic.sqlalchemy.org/en/latest/tutorial.html) for more information on creating and running migrations. + +## 3. ✅ Testing + +Before we hit the 'Wow, I'm Done' button, let's make sure everything works as expected. Run tests and make sure the existing ones don't throw a fit. And if needed, create new tests. 🕵️ + +### Run existing tests + +Running tests: +``` +uv run pytest -s tests +``` + +Running tests if you installed via pip: +``` +pytest -s tests +``` + +### Creating new tests +If you added a major feature change, please add new tests in the `tests/` directory. + +## 4. 🧩 Adding new dependencies +If you need to add a new dependency to Letta, please add the package via `uv add `. This will update the `pyproject.toml` and `uv.lock` files. If the dependency does not need to be installed by all users, make sure to mark the dependency as optional in the `pyproject.toml` file and if needed, create a new extra under `[project.optional-dependencies]`. + +## 5. 🚀 Submitting Changes + +### Check Formatting +Please ensure your code is formatted correctly by running: +``` +uv run black . -l 140 +``` + +### 🚀 Create a Pull Request + +You're almost there! It's time to share your brilliance with the world. 🌍 + +1. Visit [Letta](https://github.com/letta-ai/letta). +2. Click "New Pull Request" button. +3. Choose the base branch (`main`) and the compare branch (your feature branch). +4. Whip up a catchy title and describe your changes in the description. 🪄 + +## 6. 🔍 Review and Approval + +The maintainers will take a look and might suggest some cool upgrades or ask for more details. Once they give the thumbs up, your creation becomes part of Letta! + +## 7. 📜 Code of Conduct + +Please be sure to follow the project's Code of Conduct. + +## 8. 📫 Contact + +Need help or just want to say hi? We're here for you. Reach out through filing an issue on this GitHub repository or message us on our [Discord server](https://discord.gg/9GEQrxmVyE). + +Thanks for making Letta even more fantastic! + +## WIP - 🐋 Docker Development +If you prefer to keep your resources isolated by developing purely in containers, you can start Letta in development with: +```shell +docker compose -f compose.yaml -f development.compose.yml up +``` +This will volume mount your local codebase and reload the server on file changes. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2fee7e5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,101 @@ +# Start with pgvector base for builder +FROM pgvector/pgvector:0.8.1-pg15 AS builder +# comment to trigger ci +# Install Python and required packages +RUN apt-get update && apt-get install -y \ + python3 \ + python3-venv \ + python3-full \ + build-essential \ + libpq-dev \ + python3-dev \ + && rm -rf /var/lib/apt/lists/* + +ARG LETTA_ENVIRONMENT=DEV +ENV LETTA_ENVIRONMENT=${LETTA_ENVIRONMENT} \ + UV_NO_PROGRESS=1 \ + UV_PYTHON_PREFERENCE=system \ + UV_CACHE_DIR=/tmp/uv_cache + +# Set for other builds +ARG LETTA_VERSION +ENV LETTA_VERSION=${LETTA_VERSION} + +WORKDIR /app + +# Create and activate virtual environment +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Now install uv and uvx in the virtual environment +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/ + + +# Copy dependency files first +COPY pyproject.toml uv.lock ./ +# Then copy the rest of the application code +COPY . . + +RUN uv sync --frozen --no-dev --all-extras --python 3.11 + +# Runtime stage +FROM pgvector/pgvector:0.8.1-pg15 AS runtime + +# Overridable Node.js version with --build-arg NODE_VERSION +ARG NODE_VERSION=22 + +# Allow overriding the OpenTelemetry Collector version and let Docker inject TARGETARCH during build +ARG OTEL_VERSION=0.96.0 +ARG TARGETARCH + +RUN set -eux; \ + # Map TARGETARCH to the naming used by otel release assets + case "${TARGETARCH:-amd64}" in \ + arm64|aarch64) OTEL_ARCH=arm64 ;; \ + amd64|x86_64|x64) OTEL_ARCH=amd64 ;; \ + *) OTEL_ARCH=amd64 ;; \ + esac; \ + apt-get update && \ + # Install curl, Python, and PostgreSQL client libraries + apt-get install -y curl python3 python3-venv libpq-dev redis-server && \ + # Install Node.js + curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \ + apt-get install -y nodejs && \ + # Download and install OpenTelemetry Collector for the target architecture + OTEL_FILENAME="otelcol-contrib_${OTEL_VERSION}_linux_${OTEL_ARCH}.tar.gz"; \ + echo "Downloading https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${OTEL_VERSION}/${OTEL_FILENAME}"; \ + curl -L "https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${OTEL_VERSION}/${OTEL_FILENAME}" -o /tmp/otel-collector.tar.gz && \ + tar xzf /tmp/otel-collector.tar.gz -C /usr/local/bin && \ + rm /tmp/otel-collector.tar.gz && \ + mkdir -p /etc/otel && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Add OpenTelemetry Collector configs +COPY otel/otel-collector-config-file.yaml /etc/otel/config-file.yaml +COPY otel/otel-collector-config-clickhouse.yaml /etc/otel/config-clickhouse.yaml +COPY otel/otel-collector-config-signoz.yaml /etc/otel/config-signoz.yaml + +ARG LETTA_ENVIRONMENT=DEV +ENV LETTA_ENVIRONMENT=${LETTA_ENVIRONMENT} \ + VIRTUAL_ENV="/app/.venv" \ + PATH="/app/.venv/bin:$PATH" \ + POSTGRES_USER=letta \ + POSTGRES_PASSWORD=letta \ + POSTGRES_DB=letta + +ARG LETTA_VERSION +ENV LETTA_VERSION=${LETTA_VERSION} + +WORKDIR /app + +# Copy virtual environment and app from builder +COPY --from=builder /app . + +# Copy initialization SQL if it exists +COPY init.sql /docker-entrypoint-initdb.d/ + +EXPOSE 8283 5432 6379 4317 4318 + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] +CMD ["./letta/server/startup.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f75c342 --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2023, Letta authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..47012c3 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,206 @@ +Privacy Policy +============== + +Your privacy is critically important to us. As an overview: + +- When you use Letta applications/services/websites, we collect basic (anonymous) telemetry data such as clicks, crashes, etc. + - This data helps us understand how our users are using the Letta application(s) and it informs our roadmap of future features and buxfixes. + - If you would like to opt-out of basic telemetry, you can modify your configuration file to include `telemetry_disabled = True`. +- When you use Letta hosted services (such as the hosted endpoints or Discord Bot), we collect the data that was used to render these services. + - For example, for the hosted endpoint, this includes the message request and message response. + - We may use this data to improve our services, for example to train new models in the future. + - We do NOT collect data on any of your messages or prompts unless you are using our hosted services (for example, if you are running your own model backends, this data will never be collected). + +Below is our full Privacy Policy, which expands the overview in full detail. + +### What This Policy Covers + +This Privacy Policy applies to information that we collect about you when you use: + +- Our websites (including letta.ai, the Letta Discord server, and the repository github.com/cpacker/Letta); +- Our applications (including the Python package, Discord Bot, and any other hosted services); +- Our other Letta products, services, and features that are available on or through our websites; + +Throughout this Privacy Policy we'll refer to our websites, mobile applications, and other products and services collectively as "Services." + +Below we explain how we collect, use, and share information about you, along with the choices that you have with respect to that information. + +### Information We Collect + +We only collect information about you if we have a reason to do so — for example, to provide our Services, to communicate with you, or to make our Services better. + +We collect this information from three sources: if and when you provide information to us, automatically through operating our Services, and from outside sources. Let's go over the information that we collect. + +#### *Information You Provide to Us* + +It's probably no surprise that we collect information that you provide to us directly. Here are some examples: + +- **Basic account information:** We ask for basic information from you in order to set up your account. +- **Public profile information:** If you have an account with us, we collect the information that you provide for your public profile. +- **Credentials: **Depending on the Services you use, you may provide us with credentials for your self-hosted website (like SSH, FTP, and SFTP username and password). +- **Communications with us (hi there!):** You may also provide us with information when you post on GitHub, Discord, or message us through separate channels. + +#### *Information We Collect Automatically* + +We also collect some information automatically: + +- **Log information:** We collect information that web browsers, mobile devices, and servers typically make available, including the browser type, IP address, unique device identifiers, language preference, referring site, the date and time of access, operating system, and mobile network information. We collect log information when you use our Services. +- **Usage information:** We collect information about your usage of our Services. We use this information to, for example, provide our Services to you, get insights on how people use our Services so we can make our Services better, and understand and make predictions about user retention. +- **Location information:** We may determine the location of your device from your IP address. We collect and use this information to, for example, calculate how many people visit our Services from certain geographic regions. +- **Stored information:** We may access information stored on your devices if you upload this information to our Services. +- **Information from cookies & other technologies:** A cookie is a string of information that a website stores on a visitor's computer, and that the visitor's browser provides to the website each time the visitor returns. Pixel tags (also called web beacons) are small blocks of code placed on websites and emails. We may use cookies and other technologies like pixel tags to help us identify and track visitors, usage, and access preferences for our Services. + +#### *Information We Collect from Other Sources* + +We may also get information about you from other sources. For example: + +- **Third Party Login:** If you create or log in to our Services through another service (like Google) we'll receive associated login information (e.g. a connection token, your username, your email address) + +The information we receive depends on which services you use or authorize and what options are available. + +Third-party services may also give us information, like mailing addresses for individuals who are not yet our users (but we hope will be!). We use this information for marketing purposes like postcards and other mailers advertising our Services. + +### How and Why We Use Information + +#### *Purposes for Using Information* + +We use information about you for the purposes listed below: + +- **To provide our Services.** For example, to run a model on our hosted services to deliver a message to your client. +- **To ensure quality, maintain safety, and improve our Services.** For example, by providing automatic upgrades and new versions of our Services. Or, for example, by monitoring and analyzing how users interact with our Services so we can create new features that we think our users will enjoy and that will help them create and manage websites more efficiently or make our Services easier to use. +- **To protect our Services, our users, and the public.** For example, by detecting security incidents; detecting and protecting against malicious, deceptive, fraudulent, or illegal activity; fighting spam; complying with our legal obligations; and protecting the rights and property of Letta and others, which may result in us, for example, declining a transaction or terminating Services. +- **To fix problems with our Services.** For example, by monitoring, debugging, repairing, and preventing issues. +- **To customize the user experience.** For example, to personalize your experience by serving you relevant notifications for our Services. + +#### *Legal Bases for Collecting and Using Information* + +A note here for those in the European Union about our legal grounds for processing information about you under EU data protection laws, which is that our use of your information is based on the grounds that: + +(1) The use is necessary in order to fulfill our commitments to you under the applicable terms of service or other agreements with you or is necessary to administer your account — for example, in order to enable access to our website on your device or charge you for a paid plan; or + +(2) The use is necessary for compliance with a legal obligation; or + +(3) The use is necessary in order to protect your vital interests or those of another person; or + +(4) We have a legitimate interest in using your information — for example, to provide and update our Services; to improve our Services so that we can offer you an even better user experience; to safeguard our Services; to communicate with you; to measure, gauge, and improve the effectiveness of our advertising; and to understand our user retention and attrition; to monitor and prevent any problems with our Services; and to personalize your experience; or + +(5) You have given us your consent + +### Sharing Information + +#### *How We Share Information* + +We share information about you in limited circumstances, and with appropriate safeguards on your privacy. + +- **Subsidiaries, independent contractors, and research partners:** We may disclose information about you to our subsidiaries, independent contractors, and/or research partners who need the information to help us provide our Services or process the information on our behalf. We require our subsidiaries and independent contractors to follow this Privacy Policy for any personal information that we share with them. This includes the transfer of data collect on our Services to facilitate model training and refinement. +- **Third-party vendors:** We may share information about you with third-party vendors who need the information in order to provide their services to us, or to provide their services to you or your site. This includes vendors that help us provide our Services to you (such as intrastructure or model serving companies); those that help us understand and enhance our Services (like analytics providers); those that make tools to help us run our operations (like programs that help us with task management, scheduling, word processing, email and other communications, and collaboration among our teams); other third-party tools that help us manage operations; and companies that make products available on our websites, who may need information about you in order to, for example, provide technical or other support services to you. +- **Legal and regulatory requirements:** We may disclose information about you in response to a subpoena, court order, or other governmental request. +- **To protect rights, property, and others:** We may disclose information about you when we believe in good faith that disclosure is reasonably necessary to protect the property or rights of Letta, third parties, or the public at large. +- **Asset/IP transfers:** If any transfer of Letta assets were to happen, this Privacy Policy would continue to apply to your information and the party receiving your information may continue to use your information, but only consistent with this Privacy Policy. +- **With your consent:** We may share and disclose information with your consent or at your direction. +- **Aggregated or de-identified information:** We may share information that has been aggregated or de-identified, so that it can no longer reasonably be used to identify you. For instance, we may publish aggregate statistics about the use of our Services, or share a hashed version of your email address to facilitate customized ad campaigns on other platforms. +- **Published support requests:** If you send us a request for assistance (for example, via a support email or one of our other feedback mechanisms), we reserve the right to publish that request in order to clarify or respond to your request, or to help us support other users. + +#### *Information Shared Publicly* + +Information that you choose to make public is — you guessed it — disclosed publicly. + +That means information like your public profile, posts, other content that you make public on your website, and your "Likes" and comments on other websites are all available to others — and we hope they get a lot of views! + +For example, the photo that you upload to your public profile, or a default image if you haven't uploaded one, is your **G**lobally **R**ecognized Avatar, or Gravatar — get it? :) Your Gravatar, along with other public profile information, displays alongside the comments and "Likes" that you make on other users' websites while logged in to your WordPress.com account. Your Gravatar and public profile information may also display with your comments, "Likes," and other interactions on websites that use our Gravatar service, if the email address associated with your account is the same email address you use on the other website. + +Please keep all of this in mind when deciding what you would like to share publicly. + +### How Long We Keep Information + +We generally discard information about you when it's no longer needed for the purposes for which we collect and use it — described in the section above on How and Why We Use Information — and we're not legally required to keep it. + +### Security + +While no online service is 100% secure, we work very hard to protect information about you against unauthorized access, use, alteration, or destruction, and take reasonable measures to do so. We monitor our Services for potential vulnerabilities and attacks. To enhance the security of your account, we encourage you to enable our advanced security settings when available. + +### Choices + +You have several choices available when it comes to information about you: + +- **Opt out of telemetry:** You can opt our of basic telemetry by modifying your configuration file. +- **Limit use of hosted services:** We only retain information on model inputs/outputs when you use our hosted services. + +### Your Rights + +If you are located in certain parts of the world, including some US states and countries that fall under the scope of the European General Data Protection Regulation (aka the "GDPR"), you may have certain rights regarding your personal information, like the right to request access to or deletion of your data. + +#### *European General Data Protection Regulation (GDPR)* + +If you are located in a country that falls under the scope of the GDPR, data protection laws give you certain rights with respect to your personal data, subject to any exemptions provided by the law, including the rights to: + +- Request access to your personal data; +- Request correction or deletion of your personal data; +- Object to our use and processing of your personal data; +- Request that we limit our use and processing of your personal data; and +- Request portability of your personal data. + +You also have the right to make a complaint to a government supervisory authority. + +#### *US Privacy Laws* + +Laws in some US states, including California, Colorado, Connecticut, Utah, and Virginia, require us to provide residents with additional information about the categories of personal information we collect and share, where we get that personal information, and how and why we use it. You'll find that information in this section (if you are a California resident, please note that this is the Notice at Collection we are required to provide you under California law). + +In the last 12 months, we collected the following categories of personal information, depending on the Services used: + +- Identifiers (like your name, contact information, and device and online identifiers); +- Characteristics protected by law (for example, you might provide your gender as part of a research survey for us or you may choose to voluntarily disclose your race or veteran status); +- Internet or other electronic network activity information (such as your usage of our Services); +- Application and user data (such as model data and user inputs used to render our Services) +- Geolocation data (such as your location based on your IP address); +- Audio, electronic, visual or similar information (such as your profile picture, if you uploaded one); +- Inferences we make (such as likelihood of retention or attrition). + +We collect personal information for the purposes described in the "How and Why We Use Information section". And we share this information with the categories of third parties described in the "Sharing Information section". We retain this information for the length of time described in our "How Long We Keep Information section". + +In some US states you have additional rights subject to any exemptions provided by your state's respective law, including the right to: + +- Request a copy of the specific pieces of information we collect about you and, if you're in California, to know the categories of personal information we collect, the categories of business or commercial purpose for collecting and using it, the categories of sources from which the information came, and the categories of third parties we share it with; +- Request deletion of personal information we collect or maintain; +- Request correction of personal information we collect or maintain; +- Opt out of the sale or sharing of personal information; +- Receive a copy of your information in a readily portable format; and +- Not receive discriminatory treatment for exercising your rights. + +***Right to Opt Out*** + +Our procedures to opt-out of data collection to our Services is the "Choices" section. We do not collect or process your sensitive (and potentially sensitive) personal information except where it is strictly necessary to provide you with our service or improve our services in the future, where the processing is not for the purpose of inferring characteristics about you, or for other purposes that do not require an option to limit under California law. We don't knowingly sell or share personal information of those under 16. + +#### *Contacting Us About These Rights* + +If you'd like to contact us about one of the other rights, scroll down to "How to Reach Us" to, well, find out how to reach us. When you contact us about one of your rights under this section, we'll need to verify that you are the right person before we disclose or delete anything. For example, if you are a user, we will need you to contact us from the email address associated with your account. You can also designate an authorized agent to make a request on your behalf by giving us written authorization. We may still require you to verify your identity with us. + +#### ***Appeals Process for Rights Requests Denials*** + +In some circumstances we may deny your request to exercise one of these rights. For example, if we cannot verify that you are the account owner we may deny your request to access the personal information associated with your account. As another example, if we are legally required to maintain a copy of your personal information we may deny your request to delete your personal information. + +In the event that we deny your request, we will communicate this fact to you in writing. You may appeal our decision by responding in writing to our denial email and stating that you would like to appeal. All appeals will be reviewed by an internal expert who was not involved in your original request. In the event that your appeal is also denied this information will be communicated to you in writing. Please note that the appeal process does not apply to job applicants. + +If your appeal is denied, in some US states (Colorado, Connecticut, and Virginia) you may refer the denied appeal to the state attorney general if you believe the denial is in conflict with your legal rights. The process for how to do this will be communicated to you in writing at the same time we send you our decision about your appeal. + +### How to Reach Us + +If you have a question about this Privacy Policy, please contact us through our via [email](mailto:contact@charlespacker.com). + +### Other Things You Should Know (Keep Reading!) + +#### *Ads and Analytics Services Provided by Others* + +Ads appearing on any of our Services may be delivered by advertising networks. Othjjgger parties may also provide analytics services via our Services. These ad networks and analytics providers may set tracking technologies (like cookies) to collect information about your use of our Services and across other websites and online services. These technologies allow these third parties to recognize your device to compile information about you or others who use your device. This information allows us and other companies to, among other things, analyze and track usage, determine the popularity of certain content, and deliver ads that may be more targeted to your interests. Please note this Privacy Policy only covers the collection of information by Letta and does not cover the collection of information by any third-party advertisers or analytics providers. + +#### *Third-Party Software and Services* + +If you'd like to use third-party software or services (such as forks of our code), please keep in mind that interacting with them may mean providing information about yourself (or your site visitors) to those third parties. For example, some third-party services may request or require access to your (yours, your visitors', or customers') data via a pixel or cookie. Please note that if you use the third-party service or grant access, your data will be handled in accordance with the third party's privacy policy and practices. We don't own or control these third parties, and they have their own rules about information collection, use, and sharing, which you should review before using the software or services. + +### Privacy Policy Changes + +Although most changes are likely to be minor, we may change its Privacy Policy from time to time. We encourage visitors to frequently check this page for any changes to its Privacy Policy. If we make changes, we will notify you by revising the policy in the public repository (change log is publically viewable). Your further use of the Services after a change to our Privacy Policy will be subject to the updated policy. + +### Creative Commons Sharealike License + +This privacy policy is derived from the [Automattic Privacy Policy](https://github.com/Automattic/legalmattic) distributed under a Creative Commons Sharealike license. Thank you Automattic! diff --git a/README.md b/README.md new file mode 100644 index 0000000..131f06a --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Letta (formerly MemGPT) + +Build AI with advanced memory that can learn and self-improve over time. + +* [Letta Agent](https://docs.letta.com/letta-agent): run agents locally in your terminal, via the desktop app, or via channels like Slack +* [Letta Agent SDK](https://docs.letta.com/letta-agent-sdk/overview): build agents into your applications + +> [!NOTE] +> This repository contains the legacy Letta server (the API server behind the Letta V1 API and SDKs). Active development has moved to the [Letta Agent repo](https://github.com/letta-ai/letta-code), and self-hosting an API server is now done via the [App Server](https://docs.letta.com/letta-agent/app-server). See [AGENTS.md](AGENTS.md) for details. + +## Get started in the CLI + +Requires [Node.js 22.19+](https://nodejs.org/en/download) + +1. Install the [Letta Code](https://github.com/letta-ai/letta-code) CLI tool: `npm install -g @letta-ai/letta-code` +2. Run `letta` in your terminal to launch an agent with memory running on your local computer + +When running the CLI tool, your agent can help you code and do any task you can do on your computer. + +Letta Code supports [skills](https://docs.letta.com/letta-agent/skills) and [subagents](https://docs.letta.com/letta-agent/subagents), and bundles pre-built skills/subagents for advanced memory and continual learning. Letta is fully model-agnostic, though we recommend the latest Anthropic, OpenAI, and zAI models for best performance (see our [model leaderboard](https://leaderboard.letta.com/) for our rankings). + +## Get started with the Letta Agent SDK + +Use the [Letta Agent SDK](https://docs.letta.com/letta-agent-sdk/overview) (TypeScript) to build stateful agents into your own applications. +The SDK can run agents on [Constellation](https://docs.letta.com/letta-agent/constellation) (Letta's agent cloud), fully locally on your machine, or against a self-hosted [App Server](https://docs.letta.com/letta-agent/app-server). + +### Installation + +```bash +npm install @letta-ai/letta-agent-sdk +``` + +### Hello World example + +Below is a quick example of creating a stateful agent and streaming a conversation with it (requires a [Letta API key](https://app.letta.com/api-keys)). +See the full [quickstart guide](https://docs.letta.com/letta-agent-sdk/quickstart) for complete documentation. + +```typescript +import { LettaAgentClient } from "@letta-ai/letta-agent-sdk"; + +const client = new LettaAgentClient({ + backend: "cloud", + apiKey: process.env.LETTA_API_KEY, +}); + +// Create your agent +const agentId = await client.createAgent({ + model: "anthropic/claude-opus-4-8", + human: + "Name: Timber. Status: dog. Occupation: building Letta, infrastructure to democratize self-improving superintelligence", + persona: + "I am a self-improving superintelligence. Timber is my best friend and collaborator.", +}); + +console.log("Agent created with ID:", agentId); + +// Send your agent a message and stream the response +await using session = client.resumeSession(agentId); + +await session.send("What do you know about me?"); +for await (const message of session.stream()) { + if (message.type === "assistant") console.log(message.content); +} +``` + +To run the same agent fully locally (the SDK spawns [Letta Code](https://github.com/letta-ai/letta-code) on your machine as a subprocess), swap out the client: + +```typescript +const client = new LettaAgentClient({ backend: "local" }); +``` + +### Letta V1 SDK + +The previous-generation [V1 SDKs](https://docs.letta.com/guides/get-started/intro) (`@letta-ai/letta-client` for TypeScript, `letta-client` for Python) target the Letta API directly and are still available (see the [client SDKs](https://docs.letta.com/api-overview/client-sdks)). We recommend the Agent SDK for new projects. + +## Contributing + +Letta is an open source project built by over a hundred contributors from around the world. There are many ways to get involved in the Letta OSS project! + +* [**Join the Discord**](https://discord.gg/letta): Chat with the Letta devs and other AI developers. +* [**Chat on our forum**](https://forum.letta.com/): If you're not into Discord, check out our developer forum. +* **Follow our socials**: [Twitter/X](https://twitter.com/Letta_AI), [LinkedIn](https://www.linkedin.com/in/letta), [YouTube](https://www.youtube.com/@letta-ai) + +--- + +***Legal notices**: By using Letta and related Letta services (such as the Letta endpoint or hosted service), you are agreeing to our [privacy policy](https://www.letta.com/privacy-policy) and [terms of service](https://www.letta.com/terms-of-service).* + + diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..86812f3 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`letta-ai/letta` +- 原始仓库:https://github.com/letta-ai/letta +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..62293d7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Reporting a Vulnerability + +Please email support@letta.com with a description of the vulnerability, steps to reproduce, and any relevant details. + +Do **not** open a public issue for security vulnerabilities. + +We will acknowledge receipt within 48 hours and aim to provide +a fix or mitigation timeline within 7 days. + +## Supported Versions + +Security fixes are applied to the latest release only. diff --git a/TERMS.md b/TERMS.md new file mode 100644 index 0000000..a868db5 --- /dev/null +++ b/TERMS.md @@ -0,0 +1,42 @@ +Terms of Service +================ + +**Binding Agreement**. This is a binding contract ("Terms") between you and the developers of Letta and associated services ("we," "us," "our," "Letta developers", "Letta"). These Terms apply whenever you use any of the sites, apps, products, or services ("Services") we offer, in existence now to created in the future. Further, we may automatically upgrade our Services, and these Terms will apply to such upgrades. By accessing or using the Services, you agree to be bound by these Terms. If you use our services on behalf of an organization, you agree to these terms on behalf of that organization. If you do not agree to these Terms, you may not use the Services. + +**Privacy**. See our Privacy Policy for details on how we collect, store, and share user information. + +**Age Restrictions**. The Services are not intended for users who are under the age of 13. In order to create an account for the Services, you must be 13 years of age or older. By registering, you represent and warrant that you are 13 years of age or older. If children between the ages of 13 and 18 wish to use the Services, they must be registered by their parent or guardian. + +**Your Content and Permissions**. Content may be uploaded to, shared with, or generated by Letta -- files, videos, links, music, documents, code, and text ("Your Content"). Your Content is yours. Letta does not claim any right, title, or interest in Your Content. + +You grant us a non-exclusive, worldwide, royalty free license to do the things we need to do to provide the Services, including but not limited to storing, displaying, reproducing, and distributing Your Content. This license extends to trusted third parties we work with. + +**Content Guidelines**. You are fully responsible for Your Content. You may not copy, upload, download, or share Your Content unless you have the appropriate rights to do so. It is your responsibility to ensure that Your Content abides by applicable laws, these Terms, and with our user guidelines. We don't actively review Your Content. + +**Account Security**. You are responsible for safeguarding your password to the Services, making sure that others don't have access to it, and keeping your account information current. You must immediately notify the Letta developers of any unauthorized uses of your account or any other breaches of security. Letta will not be liable for your acts or omissions, including any damages of any kind incurred as a result of your acts or omissions. + +**Changes to these Terms**. We are constantly updating our Services, and that means sometimes we have to change the legal terms under which our Services are offered. If we make changes that are material, we will let you know, for example by posting on one of our blogs, or by sending you an email or other communication before the changes take effect. The notice will designate a reasonable period of time after which the new Terms will take effect. If you disagree with our changes, then you should stop using Letta within the designated notice period. Your continued use of Letta will be subject to the new Terms. However, any dispute that arose before the changes shall be governed by the Terms (including the binding individual arbitration clause) that were in place when the dispute arose. + +You can access archived versions of our policies at our repository. + +**DMCA Policy**. We respond to notices of alleged copyright infringement in accordance with the Digital Millennium Copyright Act ("DMCA"). If you believe that the content of a Letta account infringes your copyrights, you can notify us using the published email in our privacy policy. + +**Our Intellectual Property**: The Services and all materials contained therein, including, without limitation, Letta logo, and all designs, text, graphics, pictures, information, data, software, sound files, other files, and the selection and arrangement thereof (collectively, the "Letta Materials") are the property of Letta or its licensors or users and are protected by U.S. and international intellectual property laws. You are granted a personal, limited, non-sublicensable, non-exclusive, revocable license to access and use Letta Materials in accordance with these Terms for the sole purpose of enabling you to use and enjoy the Services. + +Other trademarks, service marks, graphics and logos used in connection with the Services may be the trademarks of other third parties. Your use of the Services grants you no right or license to reproduce or otherwise use any Letta, Letta, or third-party trademarks. + +**Termination**. You are free to stop using the Services at any time. We also reserve the right to suspend or end the Services at any time at our discretion and without notice. For example, we may suspend or terminate your use of the Services if you fail to comply with these Terms, or use the Services in a manner that would cause us legal liability, disrupt the Services, or disrupt others' use of the Services. + +**Disclaimer of Warranties**. Letta makes no warranties of any kind with respect to Letta or your use of the Services. + +**Limitation of Liability**. Letta shall not have any liability for any indirect, incidental, consequential, special, exemplary, or damages under any theory of liability arising out of, or relating to, these Terms or your use of Letta. As a condition of access to Letta, you understand and agree that Letta's liability shall not exceed $4.20. + +**Indemnification**. You agree to indemnify and hold harmless Letta, its developers, its contributors, its contractors, and its licensors, and their respective directors, officers, employees, and agents from and against any and all losses, liabilities, demands, damages, costs, claims, and expenses, including attorneys’ fees, arising out of or related to your use of our Services, including but not limited to your violation of the Agreement or any agreement with a provider of third-party services used in connection with the Services or applicable law, Content that you post, and any ecommerce activities conducted through your or another user’s website. + +**Exceptions to Agreement to Arbitrate**. Claims for injunctive or equitable relief or claims regarding intellectual property rights may be brought in any competent court without the posting of a bond. + +**No Class Actions**. You may resolve disputes with us only on an individual basis; you may not bring a claim as a plaintiff or a class member in a class, consolidated, or representative action. **Class arbitrations, class actions, private attorney general actions, and consolidation with other arbitrations are not permitted.** + +**Governing Law**. You agree that these Terms, and your use of Letta, are governed by California law, in the United States of America, without regard to its principles of conflicts of law. + +**Creative Commons Sharealike License**. This document is derived from the [Automattic legalmattic repository](https://github.com/Automattic/legalmattic) distributed under a Creative Commons Sharealike license. Thank you Automattic! diff --git a/WEBHOOK_SETUP.md b/WEBHOOK_SETUP.md new file mode 100644 index 0000000..ee3a264 --- /dev/null +++ b/WEBHOOK_SETUP.md @@ -0,0 +1,194 @@ +# Step Completion Webhook + +This feature allows you to receive webhook notifications whenever an agent step completes in the Letta agent loop. + +## Architecture + +The webhook service integrates with Letta's execution architecture in two ways: + +### 1. With Temporal (Recommended) + +When using Temporal for agent workflows, webhook calls are wrapped as Temporal activities, providing: +- Built-in retry logic with configurable timeouts +- Full observability in Temporal UI +- Durability guarantees +- Consistent error handling +- Activity history and replay capability + +Webhooks are triggered after the `create_step` activity completes in the Temporal workflow. + +### 2. Without Temporal (Direct Execution) + +For direct agent execution (non-Temporal), webhooks are called directly from the `StepManager` service methods: +- `update_step_success_async()` - When step completes successfully +- `update_step_error_async()` - When step fails with an error +- `update_step_cancelled_async()` - When step is cancelled + +Webhooks are sent after the step status is committed to the database. + +### Common Behavior + +In **both** cases: +- ✅ Webhook failures do not prevent step completion +- ✅ Step is always marked as complete in the database first +- ✅ Webhook delivery is logged for debugging +- ✅ Same authentication and payload format + +## Configuration + +Set the following environment variables to enable webhook notifications: + +### Required + +- **`STEP_COMPLETE_WEBHOOK`**: The URL endpoint that will receive POST requests when steps complete. + - Example: `https://your-app.com/api/webhooks/step-complete` + +### Optional + +- **`STEP_COMPLETE_KEY`**: A secret key used for authentication. + - When set, the webhook service will include this in an `Authorization` header as `Bearer {key}` + - Example: `your-secret-webhook-key-12345` + +## Webhook Payload + +When a step completes, the webhook service will send a POST request with the following JSON payload: + +```json +{ + "step_id": "step-01234567-89ab-cdef-0123-456789abcdef" +} +``` + +## Authentication + +If `STEP_COMPLETE_KEY` is configured, requests will include an Authorization header: + +``` +Authorization: Bearer your-secret-webhook-key-12345 +``` + +Your webhook endpoint should validate this key to ensure requests are coming from your Letta instance. + +## Example Webhook Endpoint + +Here's a simple example of a webhook endpoint (using FastAPI): + +```python +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel +import os + +app = FastAPI() + +class StepCompletePayload(BaseModel): + step_id: str + +WEBHOOK_SECRET = os.getenv("STEP_COMPLETE_KEY") + +@app.post("/api/webhooks/step-complete") +async def handle_step_complete( + payload: StepCompletePayload, + authorization: str = Header(None) +): + # Validate the webhook key + if WEBHOOK_SECRET: + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Missing authorization") + + token = authorization.replace("Bearer ", "") + if token != WEBHOOK_SECRET: + raise HTTPException(status_code=401, detail="Invalid authorization") + + # Process the step completion + print(f"Step completed: {payload.step_id}") + + # You can now: + # - Log the step completion + # - Trigger downstream processes + # - Update your application state + # - Send notifications + + return {"status": "success"} +``` + +## Usage Example + +```bash +# Set environment variables +export STEP_COMPLETE_WEBHOOK="https://your-app.com/api/webhooks/step-complete" +export STEP_COMPLETE_KEY="your-secret-webhook-key-12345" + +# Start your Letta server +python -m letta.server +``` + +## When Webhooks Are Sent + +Webhooks are triggered when a step reaches a terminal state: + +1. **Success** - Step completed successfully (`StepStatus.SUCCESS`) +2. **Error** - Step failed with an error (`StepStatus.FAILED`) +3. **Cancelled** - Step was cancelled (`StepStatus.CANCELLED`) + +All three states trigger the webhook with the same payload containing just the `step_id`. + +## Behavior + +- **No webhook URL configured**: The service will skip sending notifications (logged at debug level) +- **Webhook call succeeds**: Returns status 200-299, logged at info level +- **Webhook timeout**: Returns error, logged at warning level (does not fail the step) +- **HTTP error**: Returns non-2xx status, logged at warning level (does not fail the step) +- **Other errors**: Logged at error level (does not fail the step) + +**Important**: Webhook failures do not prevent step completion. The step will be marked as complete in the database regardless of webhook delivery status. This ensures system reliability - your webhook endpoint being down will not block agent execution. + +## Testing + +To test the webhook functionality: + +1. Set up a webhook endpoint (you can use [webhook.site](https://webhook.site) for testing) +2. Configure the environment variables +3. Run an agent and observe webhook calls when steps complete + +```bash +# Example using webhook.site +export STEP_COMPLETE_WEBHOOK="https://webhook.site/your-unique-url" +export STEP_COMPLETE_KEY="test-key-123" + +# Run tests +python -m pytest apps/core/letta/services/webhook_service_test.py -v +``` + +## Implementation Details + +The webhook notification is sent after: +1. The step is persisted to the database +2. Step metrics are recorded + +This ensures that the step data is fully committed before external systems are notified. + +### Temporal Integration + +When using Temporal, the webhook call is executed as a separate activity (`send_step_complete_webhook`) with the following configuration: + +- **Start-to-close timeout**: 15 seconds +- **Schedule-to-close timeout**: 30 seconds +- **Retry behavior**: Wrapped in try-catch to prevent workflow failure on webhook errors + +This allows you to monitor webhook delivery in the Temporal UI and get detailed visibility into any failures. + +### File Locations + +**Core Service:** +- `apps/core/letta/services/webhook_service.py` - HTTP client for webhook delivery + +**Temporal Integration:** +- `apps/core/letta/agents/temporal/activities/send_webhook.py` - Temporal activity wrapper +- `apps/core/letta/agents/temporal/temporal_agent_workflow.py` - Workflow integration +- `apps/core/letta/agents/temporal/constants.py` - Timeout constants + +**Non-Temporal Integration:** +- `apps/core/letta/services/step_manager.py` - Direct calls in update_step_* methods + +**Tests:** +- `apps/core/letta/services/webhook_service_test.py` - Unit tests diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..72cc699 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,116 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +# Use forward slashes (/) also on windows to provide an os agnostic path +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 0000000..2500aa1 --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..4202cdf --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,93 @@ +import os +from logging.config import fileConfig + +from sqlalchemy import engine_from_config, pool + +from alembic import context +from letta.config import LettaConfig +from letta.orm import Base +from letta.settings import DatabaseChoice, settings + +letta_config = LettaConfig.load() + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +if settings.database_engine is DatabaseChoice.POSTGRES: + # Convert PostgreSQL URI to sync format for alembic using common utility + from letta.database_utils import get_database_uri_for_context + + sync_pg_uri = get_database_uri_for_context(settings.letta_pg_uri, "alembic") + + config.set_main_option("sqlalchemy.url", sync_pg_uri) + print("Using database: ", sync_pg_uri) +else: + config.set_main_option("sqlalchemy.url", "sqlite:///" + os.path.join(letta_config.recall_storage_path, "sqlite.db")) + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata + +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata, include_schemas=True) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/0335b1eb9c40_add_batch_item_id_to_messages.py b/alembic/versions/0335b1eb9c40_add_batch_item_id_to_messages.py new file mode 100644 index 0000000..1c047db --- /dev/null +++ b/alembic/versions/0335b1eb9c40_add_batch_item_id_to_messages.py @@ -0,0 +1,40 @@ +"""Add batch_item_id to messages + +Revision ID: 0335b1eb9c40 +Revises: 373dabcba6cf +Create Date: 2025-05-02 10:30:08.156190 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "0335b1eb9c40" +down_revision: Union[str, None] = "373dabcba6cf" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("messages", sa.Column("batch_item_id", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("messages", "batch_item_id") + # ### end Alembic commands ### diff --git a/alembic/versions/038e68cdf0df_add_cascades_to_blocks_agents_fks_set_.py b/alembic/versions/038e68cdf0df_add_cascades_to_blocks_agents_fks_set_.py new file mode 100644 index 0000000..81f0e7d --- /dev/null +++ b/alembic/versions/038e68cdf0df_add_cascades_to_blocks_agents_fks_set_.py @@ -0,0 +1,53 @@ +"""add cascades to blocks_agents FKs; set initially immediate + +Revision ID: 038e68cdf0df +Revises: b6061da886ee +Create Date: 2025-10-07 13:01:17.872405 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "038e68cdf0df" +down_revision: Union[str, None] = "b6061da886ee" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f("blocks_agents_agent_id_fkey"), "blocks_agents", type_="foreignkey") + op.drop_constraint(op.f("fk_block_id_label"), "blocks_agents", type_="foreignkey") + op.create_foreign_key( + "fk_block_id_label", + "blocks_agents", + "block", + ["block_id", "block_label"], + ["id", "label"], + onupdate="CASCADE", + ondelete="CASCADE", + initially="IMMEDIATE", + deferrable=True, + ) + op.create_foreign_key(None, "blocks_agents", "agents", ["agent_id"], ["id"], ondelete="CASCADE") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "blocks_agents", type_="foreignkey") + op.drop_constraint("fk_block_id_label", "blocks_agents", type_="foreignkey") + op.create_foreign_key( + op.f("fk_block_id_label"), + "blocks_agents", + "block", + ["block_id", "block_label"], + ["id", "label"], + initially="DEFERRED", + deferrable=True, + ) + op.create_foreign_key(op.f("blocks_agents_agent_id_fkey"), "blocks_agents", "agents", ["agent_id"], ["id"]) + # ### end Alembic commands ### diff --git a/alembic/versions/05c3bc564286_add_metrics_to_agent_loop_runs.py b/alembic/versions/05c3bc564286_add_metrics_to_agent_loop_runs.py new file mode 100644 index 0000000..d76b064 --- /dev/null +++ b/alembic/versions/05c3bc564286_add_metrics_to_agent_loop_runs.py @@ -0,0 +1,33 @@ +"""add metrics to agent loop runs + +Revision ID: 05c3bc564286 +Revises: d007f4ca66bf +Create Date: 2025-08-06 14:30:48.255538 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "05c3bc564286" +down_revision: Union[str, None] = "d007f4ca66bf" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("jobs", sa.Column("ttft_ns", sa.BigInteger(), nullable=True)) + op.add_column("jobs", sa.Column("total_duration_ns", sa.BigInteger(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("jobs", "total_duration_ns") + op.drop_column("jobs", "ttft_ns") + # ### end Alembic commands ### diff --git a/alembic/versions/066857381578_add_approvals_field_to_messages.py b/alembic/versions/066857381578_add_approvals_field_to_messages.py new file mode 100644 index 0000000..ad88285 --- /dev/null +++ b/alembic/versions/066857381578_add_approvals_field_to_messages.py @@ -0,0 +1,41 @@ +"""add approvals field to messages + +Revision ID: 066857381578 +Revises: c734cfc0d595 +Create Date: 2025-10-09 17:56:07.333221 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +import letta.orm +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "066857381578" +down_revision: Union[str, None] = "c734cfc0d595" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + ### commands auto generated by Alembic - please adjust! ### + op.add_column("messages", sa.Column("approvals", letta.orm.custom_columns.ApprovalsColumn(), nullable=True)) + ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + ### commands auto generated by Alembic - please adjust! ### + op.drop_column("messages", "approvals") + ### end Alembic commands ### diff --git a/alembic/versions/068588268b02_add_vector_db_provider_to_archives_table.py b/alembic/versions/068588268b02_add_vector_db_provider_to_archives_table.py new file mode 100644 index 0000000..f7f0dca --- /dev/null +++ b/alembic/versions/068588268b02_add_vector_db_provider_to_archives_table.py @@ -0,0 +1,60 @@ +"""Add vector_db_provider to archives table + +Revision ID: 068588268b02 +Revises: d5103ee17ed5 +Create Date: 2025-08-27 13:16:29.428231 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "068588268b02" +down_revision: Union[str, None] = "887a4367b560" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + if settings.letta_pg_uri_no_default: + # PostgreSQL - use enum type + vectordbprovider = sa.Enum("NATIVE", "TPUF", name="vectordbprovider") + vectordbprovider.create(op.get_bind(), checkfirst=True) + + # Add column as nullable first + op.add_column("archives", sa.Column("vector_db_provider", vectordbprovider, nullable=True)) + + # Backfill existing rows with NATIVE + op.execute("UPDATE archives SET vector_db_provider = 'NATIVE' WHERE vector_db_provider IS NULL") + + # Make column non-nullable + op.alter_column("archives", "vector_db_provider", nullable=False) + else: + # SQLite - use string type + # Add column as nullable first + op.add_column("archives", sa.Column("vector_db_provider", sa.String(), nullable=True)) + + # Backfill existing rows with NATIVE + op.execute("UPDATE archives SET vector_db_provider = 'NATIVE' WHERE vector_db_provider IS NULL") + + # For SQLite, we need to recreate the table to make column non-nullable + # This is a limitation of SQLite ALTER TABLE + # For simplicity, we'll leave it nullable in SQLite + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("archives", "vector_db_provider") + + if settings.letta_pg_uri_no_default: + # Drop enum type for PostgreSQL + vectordbprovider = sa.Enum("NATIVE", "TPUF", name="vectordbprovider") + vectordbprovider.drop(op.get_bind(), checkfirst=True) + # ### end Alembic commands ### diff --git a/alembic/versions/06fbbf65d4f1_support_for_project_id_for_blocks_and_.py b/alembic/versions/06fbbf65d4f1_support_for_project_id_for_blocks_and_.py new file mode 100644 index 0000000..8dab61a --- /dev/null +++ b/alembic/versions/06fbbf65d4f1_support_for_project_id_for_blocks_and_.py @@ -0,0 +1,71 @@ +"""support for project_id for blocks and groups + +Revision ID: 06fbbf65d4f1 +Revises: f55542f37641 +Create Date: 2025-07-21 15:07:32.133538 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "06fbbf65d4f1" +down_revision: Union[str, None] = "f55542f37641" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("block", sa.Column("project_id", sa.String(), nullable=True)) + op.add_column("groups", sa.Column("project_id", sa.String(), nullable=True)) + + # NOTE: running the backfill on alembic will result in locking with running application. + # This is okay if okay with downtime. Options also to do rolling migration or dynamic updates. + + # Backfill project_id for blocks table + # Since all agents for a block have the same project_id, we can just grab the first one + # op.execute( + # text( + # """ + # UPDATE block + # SET project_id = ( + # SELECT a.project_id + # FROM blocks_agents ba + # JOIN agents a ON ba.agent_id = a.id + # WHERE ba.block_id = block.id + # AND a.project_id IS NOT NULL + # LIMIT 1 + # ) + # """ + # ) + # ) + + # Backfill project_id for groups table + # op.execute( + # text( + # """ + # UPDATE groups + # SET project_id = ( + # SELECT a.project_id + # FROM groups_agents ga + # JOIN agents a ON ga.agent_id = a.id + # WHERE ga.group_id = groups.id + # AND a.project_id IS NOT NULL + # LIMIT 1 + # ) + # """ + # ) + # ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("groups", "project_id") + op.drop_column("block", "project_id") + # ### end Alembic commands ### diff --git a/alembic/versions/08b2f8225812_adding_toolsagents_orm.py b/alembic/versions/08b2f8225812_adding_toolsagents_orm.py new file mode 100644 index 0000000..da0e190 --- /dev/null +++ b/alembic/versions/08b2f8225812_adding_toolsagents_orm.py @@ -0,0 +1,58 @@ +"""adding ToolsAgents ORM + +Revision ID: 08b2f8225812 +Revises: 3c683a662c82 +Create Date: 2024-12-05 16:46:51.258831 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "08b2f8225812" +down_revision: Union[str, None] = "3c683a662c82" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "tools_agents", + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("tool_id", sa.String(), nullable=False), + sa.Column("tool_name", sa.String(), nullable=False), + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["agent_id"], + ["agents.id"], + ), + sa.ForeignKeyConstraint(["tool_id"], ["tools.id"], name="fk_tool_id"), + sa.PrimaryKeyConstraint("agent_id", "tool_id", "tool_name", "id"), + sa.UniqueConstraint("agent_id", "tool_name", name="unique_tool_per_agent"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("tools_agents") + # ### end Alembic commands ### diff --git a/alembic/versions/0b496eae90de_add_file_agent_table.py b/alembic/versions/0b496eae90de_add_file_agent_table.py new file mode 100644 index 0000000..e522206 --- /dev/null +++ b/alembic/versions/0b496eae90de_add_file_agent_table.py @@ -0,0 +1,63 @@ +"""Add file agent table + +Revision ID: 0b496eae90de +Revises: 341068089f14 +Create Date: 2025-06-02 15:14:33.730687 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "0b496eae90de" +down_revision: Union[str, None] = "341068089f14" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "files_agents", + sa.Column("id", sa.String(), nullable=False), + sa.Column("file_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("is_open", sa.Boolean(), nullable=False), + sa.Column("visible_content", sa.Text(), nullable=True), + sa.Column("last_accessed_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["file_id"], ["files.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id", "file_id", "agent_id"), + ) + op.create_index("ix_files_agents_file_id_agent_id", "files_agents", ["file_id", "agent_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_files_agents_file_id_agent_id", table_name="files_agents") + op.drop_table("files_agents") + # ### end Alembic commands ### diff --git a/alembic/versions/0ceb975e0063_add_llm_batch_jobs_tables.py b/alembic/versions/0ceb975e0063_add_llm_batch_jobs_tables.py new file mode 100644 index 0000000..625a6e0 --- /dev/null +++ b/alembic/versions/0ceb975e0063_add_llm_batch_jobs_tables.py @@ -0,0 +1,95 @@ +"""Add LLM batch jobs tables + +Revision ID: 0ceb975e0063 +Revises: 90bb156e71df +Create Date: 2025-04-07 15:57:18.475151 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +import letta +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "0ceb975e0063" +down_revision: Union[str, None] = "90bb156e71df" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "llm_batch_job", + sa.Column("id", sa.String(), nullable=False), + sa.Column("status", sa.String(), nullable=False), + sa.Column("llm_provider", sa.String(), nullable=False), + sa.Column("create_batch_response", letta.orm.custom_columns.CreateBatchResponseColumn(), nullable=False), + sa.Column("latest_polling_response", letta.orm.custom_columns.PollBatchResponseColumn(), nullable=True), + sa.Column("last_polled_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_llm_batch_job_created_at", "llm_batch_job", ["created_at"], unique=False) + op.create_index("ix_llm_batch_job_status", "llm_batch_job", ["status"], unique=False) + op.create_table( + "llm_batch_items", + sa.Column("id", sa.String(), nullable=False), + sa.Column("batch_id", sa.String(), nullable=False), + sa.Column("llm_config", letta.orm.custom_columns.LLMConfigColumn(), nullable=False), + sa.Column("request_status", sa.String(), nullable=False), + sa.Column("step_status", sa.String(), nullable=False), + sa.Column("step_state", letta.orm.custom_columns.AgentStepStateColumn(), nullable=False), + sa.Column("batch_request_result", letta.orm.custom_columns.BatchRequestResultColumn(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["batch_id"], ["llm_batch_job.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_llm_batch_items_agent_id", "llm_batch_items", ["agent_id"], unique=False) + op.create_index("ix_llm_batch_items_batch_id", "llm_batch_items", ["batch_id"], unique=False) + op.create_index("ix_llm_batch_items_status", "llm_batch_items", ["request_status"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_llm_batch_items_status", table_name="llm_batch_items") + op.drop_index("ix_llm_batch_items_batch_id", table_name="llm_batch_items") + op.drop_index("ix_llm_batch_items_agent_id", table_name="llm_batch_items") + op.drop_table("llm_batch_items") + op.drop_index("ix_llm_batch_job_status", table_name="llm_batch_job") + op.drop_index("ix_llm_batch_job_created_at", table_name="llm_batch_job") + op.drop_table("llm_batch_job") + # ### end Alembic commands ### diff --git a/alembic/versions/15b577c62f3f_add_hidden_property_to_agents.py b/alembic/versions/15b577c62f3f_add_hidden_property_to_agents.py new file mode 100644 index 0000000..bfd99e3 --- /dev/null +++ b/alembic/versions/15b577c62f3f_add_hidden_property_to_agents.py @@ -0,0 +1,31 @@ +"""Add hidden property to agents + +Revision ID: 15b577c62f3f +Revises: 4c6c9ef0387d +Create Date: 2025-07-30 13:19:15.213121 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "15b577c62f3f" +down_revision: Union[str, None] = "4c6c9ef0387d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("agents", sa.Column("hidden", sa.Boolean(), nullable=True)) + + # Set hidden=true for existing agents with project names starting with "templates" + connection = op.get_bind() + connection.execute(sa.text("UPDATE agents SET hidden = true WHERE project_id LIKE 'templates-%'")) + + +def downgrade() -> None: + op.drop_column("agents", "hidden") diff --git a/alembic/versions/167491cfb7a8_add_identities_for_blocks.py b/alembic/versions/167491cfb7a8_add_identities_for_blocks.py new file mode 100644 index 0000000..8f0e04d --- /dev/null +++ b/alembic/versions/167491cfb7a8_add_identities_for_blocks.py @@ -0,0 +1,47 @@ +"""add identities for blocks + +Revision ID: 167491cfb7a8 +Revises: d211df879a5f +Create Date: 2025-03-07 17:51:24.843275 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "167491cfb7a8" +down_revision: Union[str, None] = "d211df879a5f" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "identities_blocks", + sa.Column("identity_id", sa.String(), nullable=False), + sa.Column("block_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["block_id"], ["block.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["identity_id"], ["identities.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("identity_id", "block_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("identities_blocks") + # ### end Alembic commands ### diff --git a/alembic/versions/175dd10fb916_add_prompt_tokens_details_to_steps.py b/alembic/versions/175dd10fb916_add_prompt_tokens_details_to_steps.py new file mode 100644 index 0000000..a796657 --- /dev/null +++ b/alembic/versions/175dd10fb916_add_prompt_tokens_details_to_steps.py @@ -0,0 +1,29 @@ +"""Add prompt_tokens_details to steps table + +Revision ID: 175dd10fb916 +Revises: b1c2d3e4f5a6 +Create Date: 2025-11-28 12:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "175dd10fb916" +down_revision: Union[str, None] = "b1c2d3e4f5a6" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add prompt_tokens_details JSON column to steps table + # This stores detailed prompt token breakdown (cached_tokens, cache_read_tokens, cache_creation_tokens) + op.add_column("steps", sa.Column("prompt_tokens_details", sa.JSON(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("steps", "prompt_tokens_details") diff --git a/alembic/versions/18e300709530_add_instructions_field_to_sources.py b/alembic/versions/18e300709530_add_instructions_field_to_sources.py new file mode 100644 index 0000000..9d730c9 --- /dev/null +++ b/alembic/versions/18e300709530_add_instructions_field_to_sources.py @@ -0,0 +1,40 @@ +"""add instructions field to sources + +Revision ID: 18e300709530 +Revises: 878607e41ca4 +Create Date: 2025-05-08 17:56:20.877183 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "18e300709530" +down_revision: Union[str, None] = "878607e41ca4" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("sources", sa.Column("instructions", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("sources", "instructions") + # ### end Alembic commands ### diff --git a/alembic/versions/18ff61fbc034_add_agent_id_index_to_mapping_tables.py b/alembic/versions/18ff61fbc034_add_agent_id_index_to_mapping_tables.py new file mode 100644 index 0000000..29e1c65 --- /dev/null +++ b/alembic/versions/18ff61fbc034_add_agent_id_index_to_mapping_tables.py @@ -0,0 +1,37 @@ +"""add agent_id index to mapping tables + +Revision ID: 18ff61fbc034 +Revises: b888f21b151f +Create Date: 2025-09-10 19:16:39.118760 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "18ff61fbc034" +down_revision: Union[str, None] = "b888f21b151f" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_index("ix_blocks_agents_block_id", "blocks_agents", ["block_id"], unique=False) + op.create_index("ix_block_label", "block", ["label"], unique=False) + op.create_index("ix_agents_organization_id", "agents", ["organization_id"], unique=False) + op.create_index("ix_tools_agents_tool_id", "tools_agents", ["tool_id"], unique=False) + op.create_index("ix_sources_agents_source_id", "sources_agents", ["source_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_sources_agents_source_id", table_name="sources_agents") + op.drop_index("ix_tools_agents_tool_id", table_name="tools_agents") + op.drop_index("ix_agents_organization_id", table_name="agents") + op.drop_index("ix_block_label", table_name="block") + op.drop_index("ix_blocks_agents_block_id", table_name="blocks_agents") + # ### end Alembic commands ### diff --git a/alembic/versions/1af251a42c06_fix_files_agents_constraints.py b/alembic/versions/1af251a42c06_fix_files_agents_constraints.py new file mode 100644 index 0000000..d95d79e --- /dev/null +++ b/alembic/versions/1af251a42c06_fix_files_agents_constraints.py @@ -0,0 +1,54 @@ +"""Fix files_agents constraints + +Revision ID: 1af251a42c06 +Revises: 51999513bcf1 +Create Date: 2025-06-30 11:50:42.200885 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "1af251a42c06" +down_revision: Union[str, None] = "51999513bcf1" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_files_agents_agent_file_name", table_name="files_agents") + op.drop_index("ix_files_agents_file_id_agent_id", table_name="files_agents") + op.drop_constraint("uq_files_agents_agent_file_name", "files_agents", type_="unique") + op.drop_constraint("uq_files_agents_file_agent", "files_agents", type_="unique") + op.create_index("ix_agent_filename", "files_agents", ["agent_id", "file_name"], unique=False) + op.create_index("ix_file_agent", "files_agents", ["file_id", "agent_id"], unique=False) + op.create_unique_constraint("uq_agent_filename", "files_agents", ["agent_id", "file_name"]) + op.create_unique_constraint("uq_file_agent", "files_agents", ["file_id", "agent_id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("uq_file_agent", "files_agents", type_="unique") + op.drop_constraint("uq_agent_filename", "files_agents", type_="unique") + op.drop_index("ix_file_agent", table_name="files_agents") + op.drop_index("ix_agent_filename", table_name="files_agents") + op.create_unique_constraint("uq_files_agents_file_agent", "files_agents", ["file_id", "agent_id"], postgresql_nulls_not_distinct=False) + op.create_unique_constraint( + "uq_files_agents_agent_file_name", "files_agents", ["agent_id", "file_name"], postgresql_nulls_not_distinct=False + ) + op.create_index("ix_files_agents_file_id_agent_id", "files_agents", ["file_id", "agent_id"], unique=False) + op.create_index("ix_files_agents_agent_file_name", "files_agents", ["agent_id", "file_name"], unique=False) + # ### end Alembic commands ### diff --git a/alembic/versions/1c28e167b74f_add_last_message_at_to_conversations.py b/alembic/versions/1c28e167b74f_add_last_message_at_to_conversations.py new file mode 100644 index 0000000..36d1c3e --- /dev/null +++ b/alembic/versions/1c28e167b74f_add_last_message_at_to_conversations.py @@ -0,0 +1,47 @@ +"""add last_message_at to conversations + +Revision ID: 1c28e167b74f +Revises: a08c972e781b +Create Date: 2026-03-22 19:07:27.266449 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "1c28e167b74f" +down_revision: Union[str, None] = "a08c972e781b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + + conversation_columns = {column["name"] for column in inspector.get_columns("conversations")} + if "last_message_at" not in conversation_columns: + op.add_column("conversations", sa.Column("last_message_at", sa.DateTime(timezone=True), nullable=True)) + + conversation_indexes = {index["name"] for index in inspector.get_indexes("conversations")} + if "ix_conversations_org_agent_last_message_at" not in conversation_indexes: + op.create_index( + "ix_conversations_org_agent_last_message_at", "conversations", ["organization_id", "agent_id", "last_message_at"], unique=False + ) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + + conversation_indexes = {index["name"] for index in inspector.get_indexes("conversations")} + if "ix_conversations_org_agent_last_message_at" in conversation_indexes: + op.drop_index("ix_conversations_org_agent_last_message_at", table_name="conversations") + + conversation_columns = {column["name"] for column in inspector.get_columns("conversations")} + if "last_message_at" in conversation_columns: + op.drop_column("conversations", "last_message_at") diff --git a/alembic/versions/1c6b6a38b713_add_pip_requirements_to_tools.py b/alembic/versions/1c6b6a38b713_add_pip_requirements_to_tools.py new file mode 100644 index 0000000..a4eff89 --- /dev/null +++ b/alembic/versions/1c6b6a38b713_add_pip_requirements_to_tools.py @@ -0,0 +1,40 @@ +"""Add pip requirements to tools + +Revision ID: 1c6b6a38b713 +Revises: c96263433aef +Create Date: 2025-06-12 18:06:54.838510 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "1c6b6a38b713" +down_revision: Union[str, None] = "c96263433aef" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("tools", sa.Column("pip_requirements", sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("tools", "pip_requirements") + # ### end Alembic commands ### diff --git a/alembic/versions/1c8880d671ee_make_an_blocks_agents_mapping_table.py b/alembic/versions/1c8880d671ee_make_an_blocks_agents_mapping_table.py new file mode 100644 index 0000000..0106236 --- /dev/null +++ b/alembic/versions/1c8880d671ee_make_an_blocks_agents_mapping_table.py @@ -0,0 +1,61 @@ +"""Make an blocks agents mapping table + +Revision ID: 1c8880d671ee +Revises: f81ceea2c08d +Create Date: 2024-11-22 15:42:47.209229 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "1c8880d671ee" +down_revision: Union[str, None] = "f81ceea2c08d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint("unique_block_id_label", "block", ["id", "label"]) + + op.create_table( + "blocks_agents", + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("block_id", sa.String(), nullable=False), + sa.Column("block_label", sa.String(), nullable=False), + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["agent_id"], + ["agents.id"], + ), + sa.ForeignKeyConstraint(["block_id", "block_label"], ["block.id", "block.label"], name="fk_block_id_label"), + sa.PrimaryKeyConstraint("agent_id", "block_id", "block_label", "id"), + sa.UniqueConstraint("agent_id", "block_label", name="unique_label_per_agent"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("unique_block_id_label", "block", type_="unique") + op.drop_table("blocks_agents") + # ### end Alembic commands ### diff --git a/alembic/versions/1dc0fee72dea_add_block_related_indexes.py b/alembic/versions/1dc0fee72dea_add_block_related_indexes.py new file mode 100644 index 0000000..489a14f --- /dev/null +++ b/alembic/versions/1dc0fee72dea_add_block_related_indexes.py @@ -0,0 +1,43 @@ +"""add block-related indexes + +Revision ID: 1dc0fee72dea +Revises: 18e300709530 +Create Date: 2025-05-12 17:06:32.055091 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "1dc0fee72dea" +down_revision: Union[str, None] = "18e300709530" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade(): + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # add index for blocks_agents table + op.create_index("ix_blocks_agents_block_label_agent_id", "blocks_agents", ["block_label", "agent_id"], unique=False) + + # add index for just block_label + op.create_index("ix_blocks_block_label", "blocks_agents", ["block_label"], unique=False) + + # add index for agent_tags for agent_id and tag + op.create_index("ix_agents_tags_agent_id_tag", "agents_tags", ["agent_id", "tag"], unique=False) + + +def downgrade(): + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.drop_index("ix_blocks_agents_block_label_agent_id", table_name="blocks_agents") + op.drop_index("ix_blocks_block_label", table_name="blocks_agents") + op.drop_index("ix_agents_tags_agent_id_tag", table_name="agents_tags") diff --git a/alembic/versions/1e553a664210_add_metadata_to_tools.py b/alembic/versions/1e553a664210_add_metadata_to_tools.py new file mode 100644 index 0000000..dd90283 --- /dev/null +++ b/alembic/versions/1e553a664210_add_metadata_to_tools.py @@ -0,0 +1,40 @@ +"""Add metadata to Tools + +Revision ID: 1e553a664210 +Revises: 2cceb07c2384 +Create Date: 2025-03-17 15:50:05.562302 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "1e553a664210" +down_revision: Union[str, None] = "2cceb07c2384" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("tools", sa.Column("metadata_", sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("tools", "metadata_") + # ### end Alembic commands ### diff --git a/alembic/versions/220856bbf43b_add_read_only_column.py b/alembic/versions/220856bbf43b_add_read_only_column.py new file mode 100644 index 0000000..52d0b89 --- /dev/null +++ b/alembic/versions/220856bbf43b_add_read_only_column.py @@ -0,0 +1,44 @@ +"""add read-only column + +Revision ID: 220856bbf43b +Revises: 1dc0fee72dea +Create Date: 2025-05-13 14:42:17.353614 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "220856bbf43b" +down_revision: Union[str, None] = "1dc0fee72dea" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # add default value of `False` + op.add_column("block", sa.Column("read_only", sa.Boolean(), nullable=True)) + op.execute( + """ + UPDATE block + SET read_only = False + """ + ) + op.alter_column("block", "read_only", nullable=False) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.drop_column("block", "read_only") diff --git a/alembic/versions/22a6e413d89c_remove_module_field_on_tool.py b/alembic/versions/22a6e413d89c_remove_module_field_on_tool.py new file mode 100644 index 0000000..1bab710 --- /dev/null +++ b/alembic/versions/22a6e413d89c_remove_module_field_on_tool.py @@ -0,0 +1,40 @@ +"""Remove module field on tool + +Revision ID: 22a6e413d89c +Revises: 88f9432739a9 +Create Date: 2025-01-10 17:38:23.811795 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "22a6e413d89c" +down_revision: Union[str, None] = "88f9432739a9" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("tools", "module") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("tools", sa.Column("module", sa.VARCHAR(), autoincrement=False, nullable=True)) + # ### end Alembic commands ### diff --git a/alembic/versions/25fc99e97839_fix_alembic_check_warnings.py b/alembic/versions/25fc99e97839_fix_alembic_check_warnings.py new file mode 100644 index 0000000..d1cb27f --- /dev/null +++ b/alembic/versions/25fc99e97839_fix_alembic_check_warnings.py @@ -0,0 +1,52 @@ +"""Remove job_usage_statistics indices and update job_messages + +Revision ID: 25fc99e97839 +Revises: f595e0e8013e +Create Date: 2025-01-16 16:48:21.000000 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "25fc99e97839" +down_revision: Union[str, None] = "f595e0e8013e" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Remove indices from job_messages + op.drop_index("ix_job_messages_created_at", table_name="job_messages") + op.drop_index("ix_job_messages_job_id", table_name="job_messages") + + # Remove indices from job_usage_statistics + op.drop_index("ix_job_usage_statistics_created_at", table_name="job_usage_statistics") + op.drop_index("ix_job_usage_statistics_job_id", table_name="job_usage_statistics") + + # Add foreign key constraint for message_id + op.create_foreign_key("fk_job_messages_message_id", "job_messages", "messages", ["message_id"], ["id"], ondelete="CASCADE") + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Remove the foreign key constraint + op.drop_constraint("fk_job_messages_message_id", "job_messages", type_="foreignkey") + + # Recreate indices for job_messages + op.create_index("ix_job_messages_job_id", "job_messages", ["job_id"]) + op.create_index("ix_job_messages_created_at", "job_messages", ["created_at"]) + + # Recreate indices for job_usage_statistics + op.create_index("ix_job_usage_statistics_job_id", "job_usage_statistics", ["job_id"]) + op.create_index("ix_job_usage_statistics_created_at", "job_usage_statistics", ["created_at"]) diff --git a/alembic/versions/27de0f58e076_add_conversations_tables_and_run_.py b/alembic/versions/27de0f58e076_add_conversations_tables_and_run_.py new file mode 100644 index 0000000..edaa469 --- /dev/null +++ b/alembic/versions/27de0f58e076_add_conversations_tables_and_run_.py @@ -0,0 +1,97 @@ +"""add conversations tables and run conversation_id + +Revision ID: 27de0f58e076 +Revises: ee2b43eea55e +Create Date: 2026-01-01 20:36:09.101274 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "27de0f58e076" +down_revision: Union[str, None] = "ee2b43eea55e" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "conversations", + sa.Column("id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("summary", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_conversations_agent_id", "conversations", ["agent_id"], unique=False) + op.create_index("ix_conversations_org_agent", "conversations", ["organization_id", "agent_id"], unique=False) + op.create_table( + "conversation_messages", + sa.Column("id", sa.String(), nullable=False), + sa.Column("conversation_id", sa.String(), nullable=True), + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("message_id", sa.String(), nullable=False), + sa.Column("position", sa.Integer(), nullable=False), + sa.Column("in_context", sa.Boolean(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["conversation_id"], ["conversations.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["message_id"], ["messages.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("conversation_id", "message_id", name="unique_conversation_message"), + ) + op.create_index("ix_conv_msg_agent_conversation", "conversation_messages", ["agent_id", "conversation_id"], unique=False) + op.create_index("ix_conv_msg_agent_id", "conversation_messages", ["agent_id"], unique=False) + op.create_index("ix_conv_msg_conversation_position", "conversation_messages", ["conversation_id", "position"], unique=False) + op.create_index("ix_conv_msg_message_id", "conversation_messages", ["message_id"], unique=False) + op.add_column("messages", sa.Column("conversation_id", sa.String(), nullable=True)) + op.create_index(op.f("ix_messages_conversation_id"), "messages", ["conversation_id"], unique=False) + op.create_foreign_key(None, "messages", "conversations", ["conversation_id"], ["id"], ondelete="SET NULL") + op.add_column("runs", sa.Column("conversation_id", sa.String(), nullable=True)) + op.create_index("ix_runs_conversation_id", "runs", ["conversation_id"], unique=False) + op.create_foreign_key(None, "runs", "conversations", ["conversation_id"], ["id"], ondelete="SET NULL") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "runs", type_="foreignkey") + op.drop_index("ix_runs_conversation_id", table_name="runs") + op.drop_column("runs", "conversation_id") + op.drop_constraint(None, "messages", type_="foreignkey") + op.drop_index(op.f("ix_messages_conversation_id"), table_name="messages") + op.drop_column("messages", "conversation_id") + op.drop_index("ix_conv_msg_message_id", table_name="conversation_messages") + op.drop_index("ix_conv_msg_conversation_position", table_name="conversation_messages") + op.drop_index("ix_conv_msg_agent_id", table_name="conversation_messages") + op.drop_index("ix_conv_msg_agent_conversation", table_name="conversation_messages") + op.drop_table("conversation_messages") + op.drop_index("ix_conversations_org_agent", table_name="conversations") + op.drop_index("ix_conversations_agent_id", table_name="conversations") + op.drop_table("conversations") + # ### end Alembic commands ### diff --git a/alembic/versions/28b8765bdd0a_add_support_for_structured_outputs_in_.py b/alembic/versions/28b8765bdd0a_add_support_for_structured_outputs_in_.py new file mode 100644 index 0000000..a76a8d0 --- /dev/null +++ b/alembic/versions/28b8765bdd0a_add_support_for_structured_outputs_in_.py @@ -0,0 +1,40 @@ +"""add support for structured_outputs in agents + +Revision ID: 28b8765bdd0a +Revises: a3c7d62e08ca +Create Date: 2025-04-18 11:43:47.701786 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "28b8765bdd0a" +down_revision: Union[str, None] = "a3c7d62e08ca" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("agents", sa.Column("response_format", sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("agents", "response_format") + # ### end Alembic commands ### diff --git a/alembic/versions/297e8217e952_nullable_embedding_for_archives_and_.py b/alembic/versions/297e8217e952_nullable_embedding_for_archives_and_.py new file mode 100644 index 0000000..69aa8f0 --- /dev/null +++ b/alembic/versions/297e8217e952_nullable_embedding_for_archives_and_.py @@ -0,0 +1,36 @@ +"""nullable embedding for archives and passages + +Revision ID: 297e8217e952 +Revises: 308a180244fc +Create Date: 2026-01-20 14:11:21.137232 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "297e8217e952" +down_revision: Union[str, None] = "308a180244fc" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("archival_passages", "embedding_config", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=True) + op.alter_column("archives", "embedding_config", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=True) + op.alter_column("source_passages", "embedding_config", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("source_passages", "embedding_config", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=False) + op.alter_column("archives", "embedding_config", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=False) + op.alter_column("archival_passages", "embedding_config", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=False) + # ### end Alembic commands ### diff --git a/alembic/versions/2c059cad97cc_create_sqlite_baseline_schema.py b/alembic/versions/2c059cad97cc_create_sqlite_baseline_schema.py new file mode 100644 index 0000000..36410d7 --- /dev/null +++ b/alembic/versions/2c059cad97cc_create_sqlite_baseline_schema.py @@ -0,0 +1,798 @@ +"""create_sqlite_baseline_schema + +Revision ID: 2c059cad97cc +Revises: 495f3f474131 +Create Date: 2025-07-16 14:34:21.280233 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "2c059cad97cc" +down_revision: Union[str, None] = "495f3f474131" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Only run this migration for SQLite + if settings.letta_pg_uri_no_default: + return + + # Create the exact schema that matches the current PostgreSQL state + # This is a snapshot of the schema at the time of this migration + # Based on the schema provided by Andy + + # Organizations table + op.create_table( + "organizations", + sa.Column("id", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("privileged_tools", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + + # Agents table + op.create_table( + "agents", + sa.Column("id", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("message_ids", sa.JSON(), nullable=True), + sa.Column("system", sa.String(), nullable=True), + sa.Column("agent_type", sa.String(), nullable=True), + sa.Column("llm_config", sa.JSON(), nullable=True), + sa.Column("embedding_config", sa.JSON(), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("tool_rules", sa.JSON(), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("project_id", sa.String(), nullable=True), + sa.Column("template_id", sa.String(), nullable=True), + sa.Column("base_template_id", sa.String(), nullable=True), + sa.Column("message_buffer_autoclear", sa.Boolean(), nullable=False), + sa.Column("enable_sleeptime", sa.Boolean(), nullable=True), + sa.Column("response_format", sa.JSON(), nullable=True), + sa.Column("last_run_completion", sa.DateTime(timezone=True), nullable=True), + sa.Column("last_run_duration_ms", sa.Integer(), nullable=True), + sa.Column("timezone", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + ) + op.create_index("ix_agents_created_at", "agents", ["created_at", "id"]) + + # Block history table (created before block table so block can reference it) + op.create_table( + "block_history", + sa.Column("id", sa.String(), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("label", sa.String(), nullable=False), + sa.Column("value", sa.Text(), nullable=False), + sa.Column("limit", sa.BigInteger(), nullable=False), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("actor_type", sa.String(), nullable=True), + sa.Column("actor_id", sa.String(), nullable=True), + sa.Column("block_id", sa.String(), nullable=False), + sa.Column("sequence_number", sa.Integer(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + # Note: block_id foreign key will be added later since block table doesn't exist yet + ) + op.create_index("ix_block_history_block_id_sequence", "block_history", ["block_id", "sequence_number"], unique=True) + + # Block table + op.create_table( + "block", + sa.Column("id", sa.String(), nullable=False), + sa.Column("value", sa.String(), nullable=False), + sa.Column("limit", sa.Integer(), nullable=False), + sa.Column("template_name", sa.String(), nullable=True), + sa.Column("label", sa.String(), nullable=False), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("is_template", sa.Boolean(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("current_history_entry_id", sa.String(), nullable=True), + sa.Column("version", sa.Integer(), server_default="1", nullable=False), + sa.Column("read_only", sa.Boolean(), nullable=False), + sa.Column("preserve_on_migration", sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.ForeignKeyConstraint(["current_history_entry_id"], ["block_history.id"], name="fk_block_current_history_entry"), + sa.UniqueConstraint("id", "label", name="unique_block_id_label"), + ) + op.create_index("created_at_label_idx", "block", ["created_at", "label"]) + op.create_index("ix_block_current_history_entry_id", "block", ["current_history_entry_id"]) + + # Note: Foreign key constraint for block_history.block_id cannot be added in SQLite after table creation + # This will be enforced at the ORM level + + # Sources table + op.create_table( + "sources", + sa.Column("id", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("embedding_config", sa.JSON(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("instructions", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.UniqueConstraint("name", "organization_id", name="uq_source_name_organization"), + ) + op.create_index("source_created_at_id_idx", "sources", ["created_at", "id"]) + + # Files table + op.create_table( + "files", + sa.Column("id", sa.String(), nullable=False), + sa.Column("source_id", sa.String(), nullable=False), + sa.Column("file_name", sa.String(), nullable=True), + sa.Column("file_path", sa.String(), nullable=True), + sa.Column("file_type", sa.String(), nullable=True), + sa.Column("file_size", sa.Integer(), nullable=True), + sa.Column("file_creation_date", sa.String(), nullable=True), + sa.Column("file_last_modified_date", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("processing_status", sa.String(), nullable=False), + sa.Column("error_message", sa.Text(), nullable=True), + sa.Column("original_file_name", sa.String(), nullable=True), + sa.Column("total_chunks", sa.Integer(), nullable=True), + sa.Column("chunks_embedded", sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["source_id"], ["sources.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + ) + # Note: SQLite doesn't support expression indexes, so these are simplified + op.create_index("ix_files_org_created", "files", ["organization_id"]) + op.create_index("ix_files_processing_status", "files", ["processing_status"]) + op.create_index("ix_files_source_created", "files", ["source_id"]) + + # Users table + op.create_table( + "users", + sa.Column("id", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + ) + + # Jobs table + op.create_table( + "jobs", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=False), + sa.Column("status", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("job_type", sa.String(), nullable=False), + sa.Column("request_config", sa.JSON(), nullable=True), + sa.Column("callback_url", sa.String(), nullable=True), + sa.Column("callback_sent_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("callback_status_code", sa.Integer(), nullable=True), + sa.Column("callback_error", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["user_id"], ["users.id"]), + ) + op.create_index("ix_jobs_created_at", "jobs", ["created_at", "id"]) + + # Tools table + op.create_table( + "tools", + sa.Column("id", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("source_type", sa.String(), nullable=False), + sa.Column("source_code", sa.String(), nullable=True), + sa.Column("json_schema", sa.JSON(), nullable=True), + sa.Column("tags", sa.JSON(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("return_char_limit", sa.Integer(), nullable=True), + sa.Column("tool_type", sa.String(), nullable=False), + sa.Column("args_json_schema", sa.JSON(), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("pip_requirements", sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.UniqueConstraint("name", "organization_id", name="uix_name_organization"), + ) + op.create_index("ix_tools_created_at_name", "tools", ["created_at", "name"]) + + # Additional tables based on Andy's schema + + # Agents tags table + op.create_table( + "agents_tags", + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("tag", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"]), + sa.UniqueConstraint("agent_id", "tag", name="unique_agent_tag"), + ) + op.create_index("ix_agents_tags_agent_id_tag", "agents_tags", ["agent_id", "tag"]) + + # Sandbox configs table + op.create_table( + "sandbox_configs", + sa.Column("id", sa.String(), nullable=False), + sa.Column("type", sa.String(), nullable=False), # sandboxtype in PG + sa.Column("config", sa.JSON(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.UniqueConstraint("type", "organization_id", name="uix_type_organization"), + ) + + # Sandbox environment variables table + op.create_table( + "sandbox_environment_variables", + sa.Column("id", sa.String(), nullable=False), + sa.Column("key", sa.String(), nullable=False), + sa.Column("value", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("sandbox_config_id", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.ForeignKeyConstraint(["sandbox_config_id"], ["sandbox_configs.id"]), + sa.UniqueConstraint("key", "sandbox_config_id", name="uix_key_sandbox_config"), + ) + + # Blocks agents table + op.create_table( + "blocks_agents", + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("block_id", sa.String(), nullable=False), + sa.Column("block_label", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"]), + sa.ForeignKeyConstraint(["block_id", "block_label"], ["block.id", "block.label"], deferrable=True, initially="DEFERRED"), + sa.UniqueConstraint("agent_id", "block_label", name="unique_label_per_agent"), + sa.UniqueConstraint("agent_id", "block_id", name="unique_agent_block"), + ) + op.create_index("ix_blocks_agents_block_label_agent_id", "blocks_agents", ["block_label", "agent_id"]) + op.create_index("ix_blocks_block_label", "blocks_agents", ["block_label"]) + + # Tools agents table + op.create_table( + "tools_agents", + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("tool_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["tool_id"], ["tools.id"], ondelete="CASCADE"), + sa.UniqueConstraint("agent_id", "tool_id", name="unique_agent_tool"), + ) + + # Sources agents table + op.create_table( + "sources_agents", + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("source_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["source_id"], ["sources.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("agent_id", "source_id"), + ) + + # Agent passages table (using BLOB for vectors in SQLite) + op.create_table( + "agent_passages", + sa.Column("id", sa.String(), nullable=False), + sa.Column("text", sa.String(), nullable=False), + sa.Column("embedding_config", sa.JSON(), nullable=False), + sa.Column("metadata_", sa.JSON(), nullable=False), + sa.Column("embedding", sa.BLOB(), nullable=True), # CommonVector becomes BLOB in SQLite + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + ) + # Note: agent_passages_org_idx is not created for SQLite as it's expected to be different + op.create_index("agent_passages_created_at_id_idx", "agent_passages", ["created_at", "id"]) + op.create_index("ix_agent_passages_org_agent", "agent_passages", ["organization_id", "agent_id"]) + + # Source passages table (using BLOB for vectors in SQLite) + op.create_table( + "source_passages", + sa.Column("id", sa.String(), nullable=False), + sa.Column("text", sa.String(), nullable=False), + sa.Column("embedding_config", sa.JSON(), nullable=False), + sa.Column("metadata_", sa.JSON(), nullable=False), + sa.Column("embedding", sa.BLOB(), nullable=True), # CommonVector becomes BLOB in SQLite + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("file_id", sa.String(), nullable=True), + sa.Column("source_id", sa.String(), nullable=False), + sa.Column("file_name", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.ForeignKeyConstraint(["file_id"], ["files.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["source_id"], ["sources.id"], ondelete="CASCADE"), + ) + # Note: source_passages_org_idx is not created for SQLite as it's expected to be different + op.create_index("source_passages_created_at_id_idx", "source_passages", ["created_at", "id"]) + + # Message sequence is handled by the sequence_id field in messages table + + # Messages table + op.create_table( + "messages", + sa.Column("id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("role", sa.String(), nullable=False), + sa.Column("text", sa.String(), nullable=True), + sa.Column("model", sa.String(), nullable=True), + sa.Column("name", sa.String(), nullable=True), + sa.Column("tool_calls", sa.JSON(), nullable=False), + sa.Column("tool_call_id", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("step_id", sa.String(), nullable=True), + sa.Column("otid", sa.String(), nullable=True), + sa.Column("tool_returns", sa.JSON(), nullable=True), + sa.Column("group_id", sa.String(), nullable=True), + sa.Column("content", sa.JSON(), nullable=True), + sa.Column("sequence_id", sa.BigInteger(), nullable=False), + sa.Column("sender_id", sa.String(), nullable=True), + sa.Column("batch_item_id", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["step_id"], ["steps.id"], ondelete="SET NULL"), + sa.UniqueConstraint("sequence_id", name="uq_messages_sequence_id"), + ) + op.create_index("ix_messages_agent_created_at", "messages", ["agent_id", "created_at"]) + op.create_index("ix_messages_created_at", "messages", ["created_at", "id"]) + op.create_index("ix_messages_agent_sequence", "messages", ["agent_id", "sequence_id"]) + op.create_index("ix_messages_org_agent", "messages", ["organization_id", "agent_id"]) + + # Create sequence table for SQLite message sequence_id generation + op.create_table( + "message_sequence", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("next_val", sa.Integer(), nullable=False, server_default="1"), + sa.PrimaryKeyConstraint("id"), + ) + + # Initialize the sequence table with the next available sequence_id + op.execute("INSERT INTO message_sequence (id, next_val) VALUES (1, 1)") + + # Now create the rest of the tables that might reference messages/steps + + # Add missing tables and columns identified from alembic check + + # Identities table + op.create_table( + "identities", + sa.Column("id", sa.String(), nullable=False), + sa.Column("identifier_key", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("identity_type", sa.String(), nullable=False), + sa.Column("project_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("properties", sa.JSON(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.UniqueConstraint("identifier_key", "project_id", "organization_id", name="unique_identifier_key_project_id_organization_id"), + ) + + # MCP Server table + op.create_table( + "mcp_server", + sa.Column("id", sa.String(), nullable=False), + sa.Column("server_name", sa.String(), nullable=False), + sa.Column("server_type", sa.String(), nullable=False), + sa.Column("server_url", sa.String(), nullable=True), + sa.Column("stdio_config", sa.JSON(), nullable=True), + sa.Column("token", sa.String(), nullable=True), + sa.Column("custom_headers", sa.JSON(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.UniqueConstraint("server_name", "organization_id", name="uix_name_organization_mcp_server"), + ) + + # Providers table + op.create_table( + "providers", + sa.Column("id", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("api_key", sa.String(), nullable=True), + sa.Column("access_key", sa.String(), nullable=True), + sa.Column("region", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("provider_type", sa.String(), nullable=True), + sa.Column("base_url", sa.String(), nullable=True), + sa.Column("provider_category", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.UniqueConstraint("name", "organization_id", name="unique_name_organization_id"), + ) + + # Agent environment variables table + op.create_table( + "agent_environment_variables", + sa.Column("id", sa.String(), nullable=False), + sa.Column("key", sa.String(), nullable=False), + sa.Column("value", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.UniqueConstraint("key", "agent_id", name="uix_key_agent"), + ) + op.create_index("idx_agent_environment_variables_agent_id", "agent_environment_variables", ["agent_id"]) + + # Groups table + op.create_table( + "groups", + sa.Column("id", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=False), + sa.Column("manager_type", sa.String(), nullable=False), + sa.Column("manager_agent_id", sa.String(), nullable=True), + sa.Column("termination_token", sa.String(), nullable=True), + sa.Column("max_turns", sa.Integer(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("agent_ids", sa.JSON(), nullable=False), + sa.Column("sleeptime_agent_frequency", sa.Integer(), nullable=True), + sa.Column("turns_counter", sa.Integer(), nullable=True), + sa.Column("last_processed_message_id", sa.String(), nullable=True), + sa.Column("max_message_buffer_length", sa.Integer(), nullable=True), + sa.Column("min_message_buffer_length", sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.ForeignKeyConstraint(["manager_agent_id"], ["agents.id"], ondelete="RESTRICT"), + ) + + # Steps table + op.create_table( + "steps", + sa.Column("id", sa.String(), nullable=False), + sa.Column("job_id", sa.String(), nullable=True), + sa.Column("completion_tokens", sa.Integer(), nullable=False, default=0), + sa.Column("prompt_tokens", sa.Integer(), nullable=False, default=0), + sa.Column("total_tokens", sa.Integer(), nullable=False, default=0), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("origin", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=True), + sa.Column("provider_id", sa.String(), nullable=True), + sa.Column("provider_name", sa.String(), nullable=True), + sa.Column("model", sa.String(), nullable=True), + sa.Column("context_window_limit", sa.Integer(), nullable=True), + sa.Column("completion_tokens_details", sa.JSON(), nullable=True), + sa.Column("tags", sa.JSON(), nullable=True), + sa.Column("tid", sa.String(), nullable=True), + sa.Column("model_endpoint", sa.String(), nullable=True), + sa.Column("trace_id", sa.String(), nullable=True), + sa.Column("agent_id", sa.String(), nullable=True), + sa.Column("provider_category", sa.String(), nullable=True), + sa.Column("feedback", sa.String(), nullable=True), + sa.Column("project_id", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["job_id"], ["jobs.id"], ondelete="SET NULL"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"], ondelete="RESTRICT"), + sa.ForeignKeyConstraint(["provider_id"], ["providers.id"], ondelete="RESTRICT"), + ) + + # Note: Foreign key constraint for block.current_history_entry_id -> block_history.id + # would need to be added here, but SQLite doesn't support ALTER TABLE ADD CONSTRAINT + # This will be handled by the ORM at runtime + + # Add missing columns to existing tables + + # All missing columns have been added to the table definitions above + + # step_id was already added in the messages table creation above + # op.add_column('messages', sa.Column('step_id', sa.String(), nullable=True)) + # op.create_foreign_key('fk_messages_step_id', 'messages', 'steps', ['step_id'], ['id'], ondelete='SET NULL') + + # Add index to source_passages for file_id + op.create_index("source_passages_file_id_idx", "source_passages", ["file_id"]) + + # Unique constraint for sources was added during table creation above + + # Create remaining association tables + + # Identities agents table + op.create_table( + "identities_agents", + sa.Column("identity_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["identity_id"], ["identities.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("identity_id", "agent_id"), + ) + + # Identities blocks table + op.create_table( + "identities_blocks", + sa.Column("identity_id", sa.String(), nullable=False), + sa.Column("block_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["identity_id"], ["identities.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["block_id"], ["block.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("identity_id", "block_id"), + ) + + # Files agents table + op.create_table( + "files_agents", + sa.Column("id", sa.String(), nullable=False), + sa.Column("file_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("source_id", sa.String(), nullable=False), + sa.Column("is_open", sa.Boolean(), nullable=False), + sa.Column("visible_content", sa.Text(), nullable=True), + sa.Column("last_accessed_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("file_name", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id", "file_id", "agent_id"), + sa.ForeignKeyConstraint(["file_id"], ["files.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["source_id"], ["sources.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.UniqueConstraint("file_id", "agent_id", name="uq_file_agent"), + sa.UniqueConstraint("agent_id", "file_name", name="uq_agent_filename"), + ) + op.create_index("ix_agent_filename", "files_agents", ["agent_id", "file_name"]) + op.create_index("ix_file_agent", "files_agents", ["file_id", "agent_id"]) + + # Groups agents table + op.create_table( + "groups_agents", + sa.Column("group_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["group_id"], ["groups.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("group_id", "agent_id"), + ) + + # Groups blocks table + op.create_table( + "groups_blocks", + sa.Column("group_id", sa.String(), nullable=False), + sa.Column("block_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["group_id"], ["groups.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["block_id"], ["block.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("group_id", "block_id"), + ) + + # LLM batch job table + op.create_table( + "llm_batch_job", + sa.Column("id", sa.String(), nullable=False), + sa.Column("status", sa.String(), nullable=False), + sa.Column("llm_provider", sa.String(), nullable=False), + sa.Column("create_batch_response", sa.JSON(), nullable=False), + sa.Column("latest_polling_response", sa.JSON(), nullable=True), + sa.Column("last_polled_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("letta_batch_job_id", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.ForeignKeyConstraint(["letta_batch_job_id"], ["jobs.id"], ondelete="CASCADE"), + ) + op.create_index("ix_llm_batch_job_created_at", "llm_batch_job", ["created_at"]) + op.create_index("ix_llm_batch_job_status", "llm_batch_job", ["status"]) + + # LLM batch items table + op.create_table( + "llm_batch_items", + sa.Column("id", sa.String(), nullable=False), + sa.Column("llm_config", sa.JSON(), nullable=False), + sa.Column("request_status", sa.String(), nullable=False), + sa.Column("step_status", sa.String(), nullable=False), + sa.Column("step_state", sa.JSON(), nullable=False), + sa.Column("batch_request_result", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("llm_batch_id", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["llm_batch_id"], ["llm_batch_job.id"], ondelete="CASCADE"), + ) + op.create_index("ix_llm_batch_items_agent_id", "llm_batch_items", ["agent_id"]) + op.create_index("ix_llm_batch_items_llm_batch_id", "llm_batch_items", ["llm_batch_id"]) + op.create_index("ix_llm_batch_items_status", "llm_batch_items", ["request_status"]) + + # Job messages table + op.create_table( + "job_messages", + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column("job_id", sa.String(), nullable=False), + sa.Column("message_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint(["job_id"], ["jobs.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["message_id"], ["messages.id"], ondelete="CASCADE"), + sa.UniqueConstraint("job_id", "message_id", name="unique_job_message"), + ) + + # File contents table + op.create_table( + "file_contents", + sa.Column("file_id", sa.String(), nullable=False), + sa.Column("text", sa.Text(), nullable=False), + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("file_id", "id"), + sa.ForeignKeyConstraint(["file_id"], ["files.id"], ondelete="CASCADE"), + sa.UniqueConstraint("file_id", name="uq_file_contents_file_id"), + ) + + # Provider traces table + op.create_table( + "provider_traces", + sa.Column("id", sa.String(), nullable=False), + sa.Column("request_json", sa.JSON(), nullable=False), + sa.Column("response_json", sa.JSON(), nullable=False), + sa.Column("step_id", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("(CURRENT_TIMESTAMP)"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("(FALSE)"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"]), + ) + op.create_index("ix_step_id", "provider_traces", ["step_id"]) + + # Complete the SQLite schema alignment by adding any remaining missing elements + try: + # Unique constraints for files_agents are already created with correct names in table definition above + + # Foreign key for files_agents.source_id is already created in table definition above + # Foreign key for messages.step_id is already created in table definition above + pass + + except Exception: + # Some operations may fail if the column/constraint already exists + # This is expected in some cases and we can continue + pass + + # Note: The remaining alembic check differences are expected for SQLite: + # 1. Type differences (BLOB vs CommonVector) - Expected and handled by ORM + # 2. Foreign key constraint differences - SQLite handles these at runtime + # 3. Index differences - SQLite doesn't support all PostgreSQL index features + # 4. Some constraint naming differences - Cosmetic differences + # + # These differences do not affect functionality as the ORM handles the abstraction + # between SQLite and PostgreSQL appropriately. + + +def downgrade() -> None: + # Only run this migration for SQLite + if settings.letta_pg_uri_no_default: + return + + # SQLite downgrade is not supported + raise NotImplementedError("SQLite downgrade is not supported. Use a fresh database instead.") diff --git a/alembic/versions/2cceb07c2384_add_content_parts_to_message.py b/alembic/versions/2cceb07c2384_add_content_parts_to_message.py new file mode 100644 index 0000000..c5e704c --- /dev/null +++ b/alembic/versions/2cceb07c2384_add_content_parts_to_message.py @@ -0,0 +1,41 @@ +"""add content parts to message + +Revision ID: 2cceb07c2384 +Revises: 77de976590ae +Create Date: 2025-03-13 14:30:53.177061 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.orm.custom_columns import MessageContentColumn +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "2cceb07c2384" +down_revision: Union[str, None] = "77de976590ae" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("messages", sa.Column("content", MessageContentColumn(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("messages", "content") + # ### end Alembic commands ### diff --git a/alembic/versions/2dbb2cf49e07_add_models_table.py b/alembic/versions/2dbb2cf49e07_add_models_table.py new file mode 100644 index 0000000..74ae3a1 --- /dev/null +++ b/alembic/versions/2dbb2cf49e07_add_models_table.py @@ -0,0 +1,66 @@ +"""add models table + +Revision ID: 2dbb2cf49e07 +Revises: a1b2c3d4e5f6 +Create Date: 2025-11-06 14:49:10.902099 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "2dbb2cf49e07" +down_revision: Union[str, None] = "a1b2c3d4e5f6" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "provider_models", + sa.Column("handle", sa.String(), nullable=False), + sa.Column("display_name", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("provider_id", sa.String(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=True), + sa.Column("model_type", sa.String(), nullable=False), + sa.Column("enabled", sa.Boolean(), server_default="TRUE", nullable=False), + sa.Column("model_endpoint_type", sa.String(), nullable=False), + sa.Column("max_context_window", sa.Integer(), nullable=True), + sa.Column("supports_token_streaming", sa.Boolean(), nullable=True), + sa.Column("supports_tool_calling", sa.Boolean(), nullable=True), + sa.Column("embedding_dim", sa.Integer(), nullable=True), + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["provider_id"], ["providers.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("handle", "organization_id", "model_type", name="unique_handle_per_org_and_type"), + sa.UniqueConstraint("name", "provider_id", "model_type", name="unique_model_per_provider_and_type"), + ) + op.create_index(op.f("ix_provider_models_handle"), "provider_models", ["handle"], unique=False) + op.create_index(op.f("ix_provider_models_model_type"), "provider_models", ["model_type"], unique=False) + op.create_index(op.f("ix_provider_models_organization_id"), "provider_models", ["organization_id"], unique=False) + op.create_index(op.f("ix_provider_models_provider_id"), "provider_models", ["provider_id"], unique=False) + op.alter_column("providers", "organization_id", existing_type=sa.VARCHAR(), nullable=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("providers", "organization_id", existing_type=sa.VARCHAR(), nullable=False) + op.drop_index(op.f("ix_provider_models_provider_id"), table_name="provider_models") + op.drop_index(op.f("ix_provider_models_organization_id"), table_name="provider_models") + op.drop_index(op.f("ix_provider_models_model_type"), table_name="provider_models") + op.drop_index(op.f("ix_provider_models_handle"), table_name="provider_models") + op.drop_table("provider_models") + # ### end Alembic commands ### diff --git a/alembic/versions/2e5e90d3cdf8_add_project_id_to_tools.py b/alembic/versions/2e5e90d3cdf8_add_project_id_to_tools.py new file mode 100644 index 0000000..0aa5dd3 --- /dev/null +++ b/alembic/versions/2e5e90d3cdf8_add_project_id_to_tools.py @@ -0,0 +1,27 @@ +"""add project_id to tools + +Revision ID: 2e5e90d3cdf8 +Revises: af842aa6f743 +Create Date: 2025-12-03 11:55:57.355341 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "2e5e90d3cdf8" +down_revision: Union[str, None] = "af842aa6f743" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("tools", sa.Column("project_id", sa.String(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("tools", "project_id") diff --git a/alembic/versions/2f4ede6ae33b_add_otid_and_tool_return_to_message.py b/alembic/versions/2f4ede6ae33b_add_otid_and_tool_return_to_message.py new file mode 100644 index 0000000..3e43ad1 --- /dev/null +++ b/alembic/versions/2f4ede6ae33b_add_otid_and_tool_return_to_message.py @@ -0,0 +1,43 @@ +"""add otid and tool return to message + +Revision ID: 2f4ede6ae33b +Revises: 54f2311edb62 +Create Date: 2025-03-05 10:04:34.717671 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +import letta.orm +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "2f4ede6ae33b" +down_revision: Union[str, None] = "54f2311edb62" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("messages", sa.Column("otid", sa.String(), nullable=True)) + op.add_column("messages", sa.Column("tool_returns", letta.orm.custom_columns.ToolReturnColumn(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("messages", "tool_returns") + op.drop_column("messages", "otid") + # ### end Alembic commands ### diff --git a/alembic/versions/308a180244fc_last_synced_column_for_providers.py b/alembic/versions/308a180244fc_last_synced_column_for_providers.py new file mode 100644 index 0000000..03aa169 --- /dev/null +++ b/alembic/versions/308a180244fc_last_synced_column_for_providers.py @@ -0,0 +1,31 @@ +"""last_synced column for providers + +Revision ID: 308a180244fc +Revises: 82feb220a9b8 +Create Date: 2026-01-05 18:54:15.996786 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "308a180244fc" +down_revision: Union[str, None] = "82feb220a9b8" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("providers", sa.Column("last_synced", sa.DateTime(timezone=True), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("providers", "last_synced") + # ### end Alembic commands ### diff --git a/alembic/versions/341068089f14_add_preserve_on_migration_to_block.py b/alembic/versions/341068089f14_add_preserve_on_migration_to_block.py new file mode 100644 index 0000000..2a7116a --- /dev/null +++ b/alembic/versions/341068089f14_add_preserve_on_migration_to_block.py @@ -0,0 +1,40 @@ +"""add preserve_on_migration to block + +Revision ID: 341068089f14 +Revises: 348214cbc081 +Create Date: 2025-05-29 10:39:44.494643 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "341068089f14" +down_revision: Union[str, None] = "348214cbc081" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("block", sa.Column("preserve_on_migration", sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("block", "preserve_on_migration") + # ### end Alembic commands ### diff --git a/alembic/versions/348214cbc081_add_org_agent_id_indices.py b/alembic/versions/348214cbc081_add_org_agent_id_indices.py new file mode 100644 index 0000000..7956115 --- /dev/null +++ b/alembic/versions/348214cbc081_add_org_agent_id_indices.py @@ -0,0 +1,40 @@ +"""add org agent id indices + +Revision ID: 348214cbc081 +Revises: dd049fbec729 +Create Date: 2025-05-28 22:43:18.509397 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "348214cbc081" +down_revision: Union[str, None] = "dd049fbec729" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_index("ix_agent_passages_org_agent", "agent_passages", ["organization_id", "agent_id"], unique=False) + op.create_index("ix_messages_org_agent", "messages", ["organization_id", "agent_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_messages_org_agent", table_name="messages") + op.drop_index("ix_agent_passages_org_agent", table_name="agent_passages") + # ### end Alembic commands ### diff --git a/alembic/versions/373dabcba6cf_add_byok_fields_and_unique_constraint.py b/alembic/versions/373dabcba6cf_add_byok_fields_and_unique_constraint.py new file mode 100644 index 0000000..6dac8e6 --- /dev/null +++ b/alembic/versions/373dabcba6cf_add_byok_fields_and_unique_constraint.py @@ -0,0 +1,44 @@ +"""add byok fields and unique constraint + +Revision ID: 373dabcba6cf +Revises: c56081a05371 +Create Date: 2025-04-30 19:38:25.010856 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "373dabcba6cf" +down_revision: Union[str, None] = "c56081a05371" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("providers", sa.Column("provider_type", sa.String(), nullable=True)) + op.add_column("providers", sa.Column("base_url", sa.String(), nullable=True)) + op.create_unique_constraint("unique_name_organization_id", "providers", ["name", "organization_id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("unique_name_organization_id", "providers", type_="unique") + op.drop_column("providers", "base_url") + op.drop_column("providers", "provider_type") + # ### end Alembic commands ### diff --git a/alembic/versions/39577145c45d_add_project_constraint_on_tools.py b/alembic/versions/39577145c45d_add_project_constraint_on_tools.py new file mode 100644 index 0000000..b9d4e86 --- /dev/null +++ b/alembic/versions/39577145c45d_add_project_constraint_on_tools.py @@ -0,0 +1,31 @@ +"""add project constraint on tools + +Revision ID: 39577145c45d +Revises: d0880aae6cee +Create Date: 2025-12-17 15:46:06.184858 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "39577145c45d" +down_revision: Union[str, None] = "d0880aae6cee" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint( + "uix_organization_project_name", "tools", ["organization_id", "project_id", "name"], postgresql_nulls_not_distinct=True + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("uix_organization_project_name", "tools", type_="unique") + # ### end Alembic commands ### diff --git a/alembic/versions/3bc3c031fbe4_create_new_runs_table_and_remove_legacy_.py b/alembic/versions/3bc3c031fbe4_create_new_runs_table_and_remove_legacy_.py new file mode 100644 index 0000000..3a19464 --- /dev/null +++ b/alembic/versions/3bc3c031fbe4_create_new_runs_table_and_remove_legacy_.py @@ -0,0 +1,31 @@ +"""create new runs table and remove legacy tables + +Revision ID: 3bc3c031fbe4 +Revises: 567e9fe06270 +Create Date: 2025-10-03 12:10:51.065067 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "3bc3c031fbe4" +down_revision: Union[str, None] = "567e9fe06270" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_index("ix_agents_project_id", "agents", ["project_id"], unique=False) + op.create_index("ix_messages_run_id", "messages", ["run_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_messages_run_id", table_name="messages") + op.drop_index("ix_agents_project_id", table_name="agents") + # ### end Alembic commands ### diff --git a/alembic/versions/3c683a662c82_migrate_jobs_to_the_orm.py b/alembic/versions/3c683a662c82_migrate_jobs_to_the_orm.py new file mode 100644 index 0000000..85a3346 --- /dev/null +++ b/alembic/versions/3c683a662c82_migrate_jobs_to_the_orm.py @@ -0,0 +1,55 @@ +"""Migrate jobs to the orm + +Revision ID: 3c683a662c82 +Revises: 5987401b40ae +Create Date: 2024-12-04 15:59:41.708396 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "3c683a662c82" +down_revision: Union[str, None] = "5987401b40ae" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("jobs", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True)) + op.add_column("jobs", sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False)) + op.add_column("jobs", sa.Column("_created_by_id", sa.String(), nullable=True)) + op.add_column("jobs", sa.Column("_last_updated_by_id", sa.String(), nullable=True)) + op.alter_column("jobs", "status", existing_type=sa.VARCHAR(), nullable=False) + op.alter_column("jobs", "completed_at", existing_type=postgresql.TIMESTAMP(timezone=True), type_=sa.DateTime(), existing_nullable=True) + op.alter_column("jobs", "user_id", existing_type=sa.VARCHAR(), nullable=False) + op.create_foreign_key(None, "jobs", "users", ["user_id"], ["id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "jobs", type_="foreignkey") + op.alter_column("jobs", "user_id", existing_type=sa.VARCHAR(), nullable=True) + op.alter_column("jobs", "completed_at", existing_type=sa.DateTime(), type_=postgresql.TIMESTAMP(timezone=True), existing_nullable=True) + op.alter_column("jobs", "status", existing_type=sa.VARCHAR(), nullable=True) + op.drop_column("jobs", "_last_updated_by_id") + op.drop_column("jobs", "_created_by_id") + op.drop_column("jobs", "is_deleted") + op.drop_column("jobs", "updated_at") + # ### end Alembic commands ### diff --git a/alembic/versions/3d2e9fb40a3c_add_indexes_for_feedback.py b/alembic/versions/3d2e9fb40a3c_add_indexes_for_feedback.py new file mode 100644 index 0000000..eb669aa --- /dev/null +++ b/alembic/versions/3d2e9fb40a3c_add_indexes_for_feedback.py @@ -0,0 +1,68 @@ +"""Add additional indexes + +Revision ID: 3d2e9fb40a3c +Revises: 57bcea83af3f +Create Date: 2025-09-20 00:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "3d2e9fb40a3c" +down_revision: Union[str, None] = "57bcea83af3f" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def _create_index_if_missing(index_name: str, table_name: str, columns: list[str], unique: bool = False) -> None: + """Create an index if it does not already exist. + + Uses SQLAlchemy inspector to avoid duplicate index errors across environments. + """ + bind = op.get_bind() + inspector = sa.inspect(bind) + existing = {ix["name"] for ix in inspector.get_indexes(table_name)} + if index_name not in existing: + op.create_index(index_name, table_name, columns, unique=unique) + + +def upgrade() -> None: + # files_agents: speed up WHERE agent_id IN (...) + _create_index_if_missing("ix_files_agents_agent_id", "files_agents", ["agent_id"]) + + # block: speed up common org+deployment filters + _create_index_if_missing( + "ix_block_organization_id_deployment_id", + "block", + ["organization_id", "deployment_id"], + ) + + # agents: speed up common org+deployment filters + _create_index_if_missing( + "ix_agents_organization_id_deployment_id", + "agents", + ["organization_id", "deployment_id"], + ) + + # Note: The index on block.current_history_entry_id (ix_block_current_history_entry_id) + # already exists from prior migrations. If drift is suspected, consider verifying + # and recreating it manually to avoid duplicate indexes under different names. + + +def downgrade() -> None: + # Drop indexes added in this migration (ignore if missing for portability) + for name, table in [ + ("ix_agents_organization_id_deployment_id", "agents"), + ("ix_block_organization_id_deployment_id", "block"), + ("ix_files_agents_agent_id", "files_agents"), + ]: + try: + op.drop_index(name, table_name=table) + except Exception: + # Be permissive in environments where indexes may have different names + pass diff --git a/alembic/versions/3e54e2fa2f7e_add_usage_columns_to_steps.py b/alembic/versions/3e54e2fa2f7e_add_usage_columns_to_steps.py new file mode 100644 index 0000000..997d0d8 --- /dev/null +++ b/alembic/versions/3e54e2fa2f7e_add_usage_columns_to_steps.py @@ -0,0 +1,33 @@ +"""add_usage_columns_to_steps + +Revision ID: 3e54e2fa2f7e +Revises: a1b2c3d4e5f8 +Create Date: 2026-02-03 16:35:51.327031 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "3e54e2fa2f7e" +down_revision: Union[str, None] = "a1b2c3d4e5f8" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("steps", sa.Column("model_handle", sa.String(), nullable=True)) + op.add_column("steps", sa.Column("cached_input_tokens", sa.Integer(), nullable=True)) + op.add_column("steps", sa.Column("cache_write_tokens", sa.Integer(), nullable=True)) + op.add_column("steps", sa.Column("reasoning_tokens", sa.Integer(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("steps", "reasoning_tokens") + op.drop_column("steps", "cache_write_tokens") + op.drop_column("steps", "cached_input_tokens") + op.drop_column("steps", "model_handle") diff --git a/alembic/versions/400501b04bf0_add_per_agent_environment_variables.py b/alembic/versions/400501b04bf0_add_per_agent_environment_variables.py new file mode 100644 index 0000000..1d42155 --- /dev/null +++ b/alembic/versions/400501b04bf0_add_per_agent_environment_variables.py @@ -0,0 +1,60 @@ +"""Add per agent environment variables + +Revision ID: 400501b04bf0 +Revises: e78b4e82db30 +Create Date: 2025-01-04 20:45:28.024690 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "400501b04bf0" +down_revision: Union[str, None] = "e78b4e82db30" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "agent_environment_variables", + sa.Column("id", sa.String(), nullable=False), + sa.Column("key", sa.String(), nullable=False), + sa.Column("value", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("key", "agent_id", name="uix_key_agent"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("agent_environment_variables") + # ### end Alembic commands ### diff --git a/alembic/versions/416b9d2db10b_repurpose_jobusagestatistics_for_new_.py b/alembic/versions/416b9d2db10b_repurpose_jobusagestatistics_for_new_.py new file mode 100644 index 0000000..1f296a7 --- /dev/null +++ b/alembic/versions/416b9d2db10b_repurpose_jobusagestatistics_for_new_.py @@ -0,0 +1,125 @@ +"""Repurpose JobUsageStatistics for new Steps table + +Revision ID: 416b9d2db10b +Revises: 25fc99e97839 +Create Date: 2025-01-17 11:27:42.115755 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "416b9d2db10b" +down_revision: Union[str, None] = "25fc99e97839" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + # Rename the table + op.rename_table("job_usage_statistics", "steps") + + # Rename the foreign key constraint and drop non-null constraint + op.alter_column("steps", "job_id", nullable=True) + op.drop_constraint("fk_job_usage_statistics_job_id", "steps", type_="foreignkey") + + # Change id field from int to string + op.execute("ALTER TABLE steps RENAME COLUMN id TO old_id") + op.add_column("steps", sa.Column("id", sa.String(), nullable=True)) + op.execute("""UPDATE steps SET id = 'step-' || gen_random_uuid()::text""") + op.drop_column("steps", "old_id") + op.alter_column("steps", "id", nullable=False) + op.create_primary_key("pk_steps_id", "steps", ["id"]) + + # Add new columns + op.add_column("steps", sa.Column("origin", sa.String(), nullable=True)) + op.add_column("steps", sa.Column("organization_id", sa.String(), nullable=True)) + op.add_column("steps", sa.Column("provider_id", sa.String(), nullable=True)) + op.add_column("steps", sa.Column("provider_name", sa.String(), nullable=True)) + op.add_column("steps", sa.Column("model", sa.String(), nullable=True)) + op.add_column("steps", sa.Column("context_window_limit", sa.Integer(), nullable=True)) + op.add_column( + "steps", + sa.Column("completion_tokens_details", postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), + ) + op.add_column( + "steps", + sa.Column("tags", postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), + ) + op.add_column("steps", sa.Column("tid", sa.String(), nullable=True)) + + # Add new foreign key constraint for provider_id + op.create_foreign_key("fk_steps_organization_id", "steps", "providers", ["provider_id"], ["id"], ondelete="RESTRICT") + + # Add new foreign key constraint for provider_id + op.create_foreign_key("fk_steps_provider_id", "steps", "organizations", ["organization_id"], ["id"], ondelete="RESTRICT") + + # Add new foreign key constraint for provider_id + op.create_foreign_key("fk_steps_job_id", "steps", "jobs", ["job_id"], ["id"], ondelete="SET NULL") + + # Drop old step_id and step_count columns which aren't in the new model + op.drop_column("steps", "step_id") + op.drop_column("steps", "step_count") + + # Add step_id to messages table + op.add_column("messages", sa.Column("step_id", sa.String(), nullable=True)) + op.create_foreign_key("fk_messages_step_id", "messages", "steps", ["step_id"], ["id"], ondelete="SET NULL") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + # Remove step_id from messages first to avoid foreign key conflicts + op.drop_constraint("fk_messages_step_id", "messages", type_="foreignkey") + op.drop_column("messages", "step_id") + + # Restore old step_count and step_id column + op.add_column("steps", sa.Column("step_count", sa.Integer(), nullable=True)) + op.add_column("steps", sa.Column("step_id", sa.String(), nullable=True)) + + # Drop new columns and constraints + op.drop_constraint("fk_steps_provider_id", "steps", type_="foreignkey") + op.drop_constraint("fk_steps_organization_id", "steps", type_="foreignkey") + op.drop_constraint("fk_steps_job_id", "steps", type_="foreignkey") + + op.drop_column("steps", "tid") + op.drop_column("steps", "tags") + op.drop_column("steps", "completion_tokens_details") + op.drop_column("steps", "context_window_limit") + op.drop_column("steps", "model") + op.drop_column("steps", "provider_name") + op.drop_column("steps", "provider_id") + op.drop_column("steps", "organization_id") + op.drop_column("steps", "origin") + + # Add constraints back + op.execute("DELETE FROM steps WHERE job_id IS NULL") + op.alter_column("steps", "job_id", nullable=False) + op.create_foreign_key("fk_job_usage_statistics_job_id", "steps", "jobs", ["job_id"], ["id"], ondelete="CASCADE") + + # Change id field from string back to int + op.add_column("steps", sa.Column("old_id", sa.Integer(), nullable=True)) + op.execute("""UPDATE steps SET old_id = CAST(ABS(hashtext(REPLACE(id, 'step-', '')::text)) AS integer)""") + op.drop_column("steps", "id") + op.execute("ALTER TABLE steps RENAME COLUMN old_id TO id") + op.alter_column("steps", "id", nullable=False) + op.create_primary_key("pk_steps_id", "steps", ["id"]) + + # Rename the table + op.rename_table("steps", "job_usage_statistics") + # ### end Alembic commands ### diff --git a/alembic/versions/4537f0996495_add_start_end_for_agent_file.py b/alembic/versions/4537f0996495_add_start_end_for_agent_file.py new file mode 100644 index 0000000..488bb0d --- /dev/null +++ b/alembic/versions/4537f0996495_add_start_end_for_agent_file.py @@ -0,0 +1,33 @@ +"""Add start end for agent file + +Revision ID: 4537f0996495 +Revises: 06fbbf65d4f1 +Create Date: 2025-07-25 17:44:26.748765 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "4537f0996495" +down_revision: Union[str, None] = "06fbbf65d4f1" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("files_agents", sa.Column("start_line", sa.Integer(), nullable=True)) + op.add_column("files_agents", sa.Column("end_line", sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("files_agents", "end_line") + op.drop_column("files_agents", "start_line") + # ### end Alembic commands ### diff --git a/alembic/versions/45402909a46b_add_concurrent_indexes_for_messages_.py b/alembic/versions/45402909a46b_add_concurrent_indexes_for_messages_.py new file mode 100644 index 0000000..00651ba --- /dev/null +++ b/alembic/versions/45402909a46b_add_concurrent_indexes_for_messages_.py @@ -0,0 +1,43 @@ +"""add concurrent indexes for messages listing + +Revision ID: 45402909a46b +Revises: b2c3d4e5f6a8 +Create Date: 2026-03-03 21:23:50.802684 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "45402909a46b" +down_revision: Union[str, None] = "b2c3d4e5f6a8" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + connection = op.get_bind() + connection.commit() + autocommit_connection = connection.execution_options(isolation_level="AUTOCOMMIT") + autocommit_connection.exec_driver_sql( + """ + CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_messages_on_updated_at + ON messages USING btree (updated_at) + """ + ) + autocommit_connection.exec_driver_sql( + """ + CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_messages_agent_conversation_sequence + ON messages USING btree (agent_id, conversation_id, sequence_id) + """ + ) + + +def downgrade() -> None: + connection = op.get_bind() + connection.commit() + autocommit_connection = connection.execution_options(isolation_level="AUTOCOMMIT") + autocommit_connection.exec_driver_sql("DROP INDEX CONCURRENTLY IF EXISTS ix_messages_agent_conversation_sequence") + autocommit_connection.exec_driver_sql("DROP INDEX CONCURRENTLY IF EXISTS idx_messages_on_updated_at") diff --git a/alembic/versions/46699adc71a7_add_unique_constraint_to_source_names_.py b/alembic/versions/46699adc71a7_add_unique_constraint_to_source_names_.py new file mode 100644 index 0000000..3578932 --- /dev/null +++ b/alembic/versions/46699adc71a7_add_unique_constraint_to_source_names_.py @@ -0,0 +1,77 @@ +"""Add unique constraint to source names and also add original file name column + +Revision ID: 46699adc71a7 +Revises: 1af251a42c06 +Create Date: 2025-07-01 13:30:48.279151 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "46699adc71a7" +down_revision: Union[str, None] = "1af251a42c06" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("files", sa.Column("original_file_name", sa.String(), nullable=True)) + + # Handle existing duplicate source names before adding unique constraint + connection = op.get_bind() + + # Find duplicates and rename them by appending a suffix + result = connection.execute( + sa.text( + """ + WITH duplicates AS ( + SELECT name, organization_id, + ROW_NUMBER() OVER (PARTITION BY name, organization_id ORDER BY created_at) as rn, + id + FROM sources + WHERE (name, organization_id) IN ( + SELECT name, organization_id + FROM sources + GROUP BY name, organization_id + HAVING COUNT(*) > 1 + ) + ) + SELECT id, name, rn + FROM duplicates + WHERE rn > 1 + """ + ) + ) + + # Rename duplicates by appending a number suffix + for row in result: + source_id, original_name, duplicate_number = row + new_name = f"{original_name}_{duplicate_number}" + connection.execute( + sa.text("UPDATE sources SET name = :new_name WHERE id = :source_id"), {"new_name": new_name, "source_id": source_id} + ) + + op.create_unique_constraint("uq_source_name_organization", "sources", ["name", "organization_id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("uq_source_name_organization", "sources", type_="unique") + op.drop_column("files", "original_file_name") + # ### end Alembic commands ### diff --git a/alembic/versions/47d2277e530d_add_total_chunks_and_chunks_embedded_to_.py b/alembic/versions/47d2277e530d_add_total_chunks_and_chunks_embedded_to_.py new file mode 100644 index 0000000..f3a40d2 --- /dev/null +++ b/alembic/versions/47d2277e530d_add_total_chunks_and_chunks_embedded_to_.py @@ -0,0 +1,42 @@ +"""Add total_chunks and chunks_embedded to files + +Revision ID: 47d2277e530d +Revises: 56254216524f +Create Date: 2025-07-03 14:32:08.539280 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "47d2277e530d" +down_revision: Union[str, None] = "56254216524f" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("files", sa.Column("total_chunks", sa.Integer(), nullable=True)) + op.add_column("files", sa.Column("chunks_embedded", sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("files", "chunks_embedded") + op.drop_column("files", "total_chunks") + # ### end Alembic commands ### diff --git a/alembic/versions/495f3f474131_write_source_id_directly_to_files_agents.py b/alembic/versions/495f3f474131_write_source_id_directly_to_files_agents.py new file mode 100644 index 0000000..bb5e9dd --- /dev/null +++ b/alembic/versions/495f3f474131_write_source_id_directly_to_files_agents.py @@ -0,0 +1,61 @@ +"""Write source_id directly to files agents + +Revision ID: 495f3f474131 +Revises: 47d2277e530d +Create Date: 2025-07-10 17:14:45.154738 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "495f3f474131" +down_revision: Union[str, None] = "47d2277e530d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + # Step 1: Add the column as nullable first + op.add_column("files_agents", sa.Column("source_id", sa.String(), nullable=True)) + + # Step 2: Backfill source_id from files table + connection = op.get_bind() + connection.execute( + sa.text( + """ + UPDATE files_agents + SET source_id = files.source_id + FROM files + WHERE files_agents.file_id = files.id + """ + ) + ) + + # Step 3: Make the column NOT NULL now that it's populated + op.alter_column("files_agents", "source_id", nullable=False) + + # Step 4: Add the foreign key constraint + op.create_foreign_key(None, "files_agents", "sources", ["source_id"], ["id"], ondelete="CASCADE") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "files_agents", type_="foreignkey") + op.drop_column("files_agents", "source_id") + # ### end Alembic commands ### diff --git a/alembic/versions/4c6c9ef0387d_support_modal_sandbox_type.py b/alembic/versions/4c6c9ef0387d_support_modal_sandbox_type.py new file mode 100644 index 0000000..652b355 --- /dev/null +++ b/alembic/versions/4c6c9ef0387d_support_modal_sandbox_type.py @@ -0,0 +1,55 @@ +"""support modal sandbox type + +Revision ID: 4c6c9ef0387d +Revises: 4537f0996495 +Create Date: 2025-07-29 15:10:08.996251 + +""" + +from typing import Sequence, Union + +from sqlalchemy import text + +from alembic import op +from letta.settings import DatabaseChoice, settings + +# revision identifiers, used by Alembic. +revision: str = "4c6c9ef0387d" +down_revision: Union[str, None] = "4537f0996495" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # SQLite just uses strings + if settings.database_engine == DatabaseChoice.POSTGRES: + op.execute("ALTER TYPE sandboxtype ADD VALUE 'MODAL' AFTER 'E2B'") + + +def downgrade() -> None: + if settings.database_engine == DatabaseChoice.POSTGRES: + connection = op.get_bind() + + data_conflicts = connection.execute( + text( + """ + SELECT COUNT(*) + FROM sandbox_configs + WHERE "type" NOT IN ('E2B', 'LOCAL') + """ + ) + ).fetchone() + if data_conflicts[0]: + raise RuntimeError( + ( + "Cannot downgrade enum: Data conflicts are detected in sandbox_configs.sandboxtype.\n" + "Please manually handle these records before handling the downgrades.\n" + f"{data_conflicts} invalid sandboxtype values" + ) + ) + + # Postgres does not support dropping enum values. Create a new enum and swap them. + op.execute("CREATE TYPE sandboxtype_old AS ENUM ('E2B', 'LOCAL')") + op.execute('ALTER TABLE sandbox_configs ALTER COLUMN "type" TYPE sandboxtype_old USING "type"::text::sandboxtype_old') + op.execute("DROP TYPE sandboxtype") + op.execute("ALTER TYPE sandboxtype_old RENAME to sandboxtype") diff --git a/alembic/versions/4e88e702f85e_drop_api_tokens_table_in_oss.py b/alembic/versions/4e88e702f85e_drop_api_tokens_table_in_oss.py new file mode 100644 index 0000000..0bd56eb --- /dev/null +++ b/alembic/versions/4e88e702f85e_drop_api_tokens_table_in_oss.py @@ -0,0 +1,51 @@ +"""Drop api tokens table in OSS + +Revision ID: 4e88e702f85e +Revises: d05669b60ebe +Create Date: 2024-12-13 17:19:55.796210 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "4e88e702f85e" +down_revision: Union[str, None] = "d05669b60ebe" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("tokens_idx_key", table_name="tokens") + op.drop_index("tokens_idx_user", table_name="tokens") + op.drop_table("tokens") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "tokens", + sa.Column("id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("user_id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("key", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint("id", name="tokens_pkey"), + ) + op.create_index("tokens_idx_user", "tokens", ["user_id"], unique=False) + op.create_index("tokens_idx_key", "tokens", ["key"], unique=False) + # ### end Alembic commands ### diff --git a/alembic/versions/51999513bcf1_steps_feedback_field.py b/alembic/versions/51999513bcf1_steps_feedback_field.py new file mode 100644 index 0000000..d20f248 --- /dev/null +++ b/alembic/versions/51999513bcf1_steps_feedback_field.py @@ -0,0 +1,40 @@ +"""steps feedback field + +Revision ID: 51999513bcf1 +Revises: 61ee53ec45a5 +Create Date: 2025-06-20 14:09:22.993263 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "51999513bcf1" +down_revision: Union[str, None] = "c7ac45f69849" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("steps", sa.Column("feedback", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("steps", "feedback") + # ### end Alembic commands ### diff --git a/alembic/versions/539afa667cff_add_telemetry_context_fields_to_.py b/alembic/versions/539afa667cff_add_telemetry_context_fields_to_.py new file mode 100644 index 0000000..4e54ae0 --- /dev/null +++ b/alembic/versions/539afa667cff_add_telemetry_context_fields_to_.py @@ -0,0 +1,33 @@ +"""add telemetry context fields to provider_traces + +Revision ID: 539afa667cff +Revises: a1b2c3d4e5f7 +Create Date: 2026-01-16 18:29:29.811385 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "539afa667cff" +down_revision: Union[str, None] = "a1b2c3d4e5f7" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("provider_traces", sa.Column("agent_id", sa.String(), nullable=True)) + op.add_column("provider_traces", sa.Column("agent_tags", sa.JSON(), nullable=True)) + op.add_column("provider_traces", sa.Column("call_type", sa.String(), nullable=True)) + op.add_column("provider_traces", sa.Column("run_id", sa.String(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("provider_traces", "run_id") + op.drop_column("provider_traces", "call_type") + op.drop_column("provider_traces", "agent_tags") + op.drop_column("provider_traces", "agent_id") diff --git a/alembic/versions/549eff097c71_update_identities_unique_constraint_and_.py b/alembic/versions/549eff097c71_update_identities_unique_constraint_and_.py new file mode 100644 index 0000000..45073c7 --- /dev/null +++ b/alembic/versions/549eff097c71_update_identities_unique_constraint_and_.py @@ -0,0 +1,98 @@ +"""update identities unique constraint and properties + +Revision ID: 549eff097c71 +Revises: a3047a624130 +Create Date: 2025-02-20 09:53:42.743105 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "549eff097c71" +down_revision: Union[str, None] = "a3047a624130" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + # Update unique constraint on identities table + op.drop_constraint("unique_identifier_pid_org_id", "identities", type_="unique") + op.create_unique_constraint( + "unique_identifier_without_project", + "identities", + ["identifier_key", "project_id", "organization_id"], + postgresql_nulls_not_distinct=True, + ) + + # Add properties column to identities table + op.add_column("identities", sa.Column("properties", postgresql.JSONB, nullable=False, server_default="[]")) + + # Create identities_agents table for many-to-many relationship + op.create_table( + "identities_agents", + sa.Column("identity_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["identity_id"], ["identities.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("identity_id", "agent_id"), + ) + + # Migrate existing relationships + # First, get existing relationships where identity_id is not null + op.execute( + """ + INSERT INTO identities_agents (identity_id, agent_id) + SELECT DISTINCT identity_id, id as agent_id + FROM agents + WHERE identity_id IS NOT NULL + """ + ) + + # Remove old identity_id column from agents + op.drop_column("agents", "identity_id") + op.drop_column("agents", "identifier_key") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + # Add back the old columns to agents + op.add_column("agents", sa.Column("identity_id", sa.String(), nullable=True)) + op.add_column("agents", sa.Column("identifier_key", sa.String(), nullable=True)) + + # Migrate relationships back + op.execute( + """ + UPDATE agents a + SET identity_id = ia.identity_id + FROM identities_agents ia + WHERE a.id = ia.agent_id + """ + ) + + # Drop the many-to-many table + op.drop_table("identities_agents") + + # Drop properties column + op.drop_column("identities", "properties") + + # Restore old unique constraint + op.drop_constraint("unique_identifier_without_project", "identities", type_="unique") + op.create_unique_constraint("unique_identifier_pid_org_id", "identities", ["identifier_key", "project_id", "organization_id"]) + # ### end Alembic commands ### diff --git a/alembic/versions/54c76f7cabca_add_tags_to_passages_and_create_passage_.py b/alembic/versions/54c76f7cabca_add_tags_to_passages_and_create_passage_.py new file mode 100644 index 0000000..0cfa65f --- /dev/null +++ b/alembic/versions/54c76f7cabca_add_tags_to_passages_and_create_passage_.py @@ -0,0 +1,73 @@ +"""Add tags to passages and create passage_tags junction table + +Revision ID: 54c76f7cabca +Revises: c41c87205254 +Create Date: 2025-08-28 15:13:01.549590 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "54c76f7cabca" +down_revision: Union[str, None] = "c41c87205254" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + # Database-specific timestamp defaults + if not settings.letta_pg_uri_no_default: + # SQLite uses CURRENT_TIMESTAMP + timestamp_default = sa.text("(CURRENT_TIMESTAMP)") + else: + # PostgreSQL uses now() + timestamp_default = sa.text("now()") + + op.create_table( + "passage_tags", + sa.Column("id", sa.String(), nullable=False), + sa.Column("tag", sa.String(), nullable=False), + sa.Column("passage_id", sa.String(), nullable=False), + sa.Column("archive_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=timestamp_default, nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=timestamp_default, nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["archive_id"], ["archives.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.ForeignKeyConstraint(["passage_id"], ["archival_passages.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("passage_id", "tag", name="uq_passage_tag"), + ) + op.create_index("ix_passage_tags_archive_id", "passage_tags", ["archive_id"], unique=False) + op.create_index("ix_passage_tags_archive_tag", "passage_tags", ["archive_id", "tag"], unique=False) + op.create_index("ix_passage_tags_org_archive", "passage_tags", ["organization_id", "archive_id"], unique=False) + op.create_index("ix_passage_tags_tag", "passage_tags", ["tag"], unique=False) + op.add_column("archival_passages", sa.Column("tags", sa.JSON(), nullable=True)) + op.add_column("source_passages", sa.Column("tags", sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("source_passages", "tags") + op.drop_column("archival_passages", "tags") + op.drop_index("ix_passage_tags_tag", table_name="passage_tags") + op.drop_index("ix_passage_tags_org_archive", table_name="passage_tags") + op.drop_index("ix_passage_tags_archive_tag", table_name="passage_tags") + op.drop_index("ix_passage_tags_archive_id", table_name="passage_tags") + op.drop_table("passage_tags") + # ### end Alembic commands ### diff --git a/alembic/versions/54dec07619c4_divide_passage_table_into_.py b/alembic/versions/54dec07619c4_divide_passage_table_into_.py new file mode 100644 index 0000000..e8c85fe --- /dev/null +++ b/alembic/versions/54dec07619c4_divide_passage_table_into_.py @@ -0,0 +1,122 @@ +"""divide passage table into SourcePassages and AgentPassages + +Revision ID: 54dec07619c4 +Revises: 4e88e702f85e +Create Date: 2024-12-14 17:23:08.772554 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from pgvector.sqlalchemy import Vector +from sqlalchemy.dialects import postgresql + +from alembic import op +from letta.orm.custom_columns import EmbeddingConfigColumn +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "54dec07619c4" +down_revision: Union[str, None] = "4e88e702f85e" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + from pgvector.sqlalchemy import Vector + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "agent_passages", + sa.Column("id", sa.String(), nullable=False), + sa.Column("text", sa.String(), nullable=False), + sa.Column("embedding_config", EmbeddingConfigColumn(), nullable=False), + sa.Column("metadata_", sa.JSON(), nullable=False), + sa.Column("embedding", Vector(dim=4096), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("agent_passages_org_idx", "agent_passages", ["organization_id"], unique=False) + op.create_table( + "source_passages", + sa.Column("id", sa.String(), nullable=False), + sa.Column("text", sa.String(), nullable=False), + sa.Column("embedding_config", EmbeddingConfigColumn(), nullable=False), + sa.Column("metadata_", sa.JSON(), nullable=False), + sa.Column("embedding", Vector(dim=4096), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("file_id", sa.String(), nullable=True), + sa.Column("source_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["file_id"], ["files.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.ForeignKeyConstraint(["source_id"], ["sources.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("source_passages_org_idx", "source_passages", ["organization_id"], unique=False) + op.drop_table("passages") + op.drop_constraint("files_source_id_fkey", "files", type_="foreignkey") + op.create_foreign_key(None, "files", "sources", ["source_id"], ["id"], ondelete="CASCADE") + op.drop_constraint("messages_agent_id_fkey", "messages", type_="foreignkey") + op.create_foreign_key(None, "messages", "agents", ["agent_id"], ["id"], ondelete="CASCADE") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "messages", type_="foreignkey") + op.create_foreign_key("messages_agent_id_fkey", "messages", "agents", ["agent_id"], ["id"]) + op.drop_constraint(None, "files", type_="foreignkey") + op.create_foreign_key("files_source_id_fkey", "files", "sources", ["source_id"], ["id"]) + op.create_table( + "passages", + sa.Column("id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("text", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("file_id", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("agent_id", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("source_id", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("embedding", Vector(dim=4096), autoincrement=False, nullable=True), + sa.Column("embedding_config", postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), + sa.Column("metadata_", postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), + sa.Column("created_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), + sa.Column("updated_at", postgresql.TIMESTAMP(timezone=True), server_default=sa.text("now()"), autoincrement=False, nullable=True), + sa.Column("is_deleted", sa.BOOLEAN(), server_default=sa.text("false"), autoincrement=False, nullable=False), + sa.Column("_created_by_id", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("_last_updated_by_id", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("organization_id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], name="passages_agent_id_fkey"), + sa.ForeignKeyConstraint(["file_id"], ["files.id"], name="passages_file_id_fkey", ondelete="CASCADE"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"], name="passages_organization_id_fkey"), + sa.PrimaryKeyConstraint("id", name="passages_pkey"), + ) + op.drop_index("source_passages_org_idx", table_name="source_passages") + op.drop_table("source_passages") + op.drop_index("agent_passages_org_idx", table_name="agent_passages") + op.drop_table("agent_passages") + # ### end Alembic commands ### diff --git a/alembic/versions/54f2311edb62_add_args_schema_to_tools.py b/alembic/versions/54f2311edb62_add_args_schema_to_tools.py new file mode 100644 index 0000000..163a4e8 --- /dev/null +++ b/alembic/versions/54f2311edb62_add_args_schema_to_tools.py @@ -0,0 +1,40 @@ +"""add args schema to tools + +Revision ID: 54f2311edb62 +Revises: b183663c6769 +Create Date: 2025-02-27 16:45:50.835081 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "54f2311edb62" +down_revision: Union[str, None] = "b183663c6769" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("tools", sa.Column("args_json_schema", sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("tools", "args_json_schema") + # ### end Alembic commands ### diff --git a/alembic/versions/56254216524f_add_custom_headers_to_mcp_server.py b/alembic/versions/56254216524f_add_custom_headers_to_mcp_server.py new file mode 100644 index 0000000..80c5753 --- /dev/null +++ b/alembic/versions/56254216524f_add_custom_headers_to_mcp_server.py @@ -0,0 +1,40 @@ +"""add_custom_headers_to_mcp_server + +Revision ID: 56254216524f +Revises: 60ed28ee7138 +Create Date: 2025-07-02 14:08:59.163861 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "56254216524f" +down_revision: Union[str, None] = "60ed28ee7138" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("mcp_server", sa.Column("custom_headers", sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("mcp_server", "custom_headers") + # ### end Alembic commands ### diff --git a/alembic/versions/567e9fe06270_create_new_runs_table_and_remove_legacy_.py b/alembic/versions/567e9fe06270_create_new_runs_table_and_remove_legacy_.py new file mode 100644 index 0000000..aa4cf90 --- /dev/null +++ b/alembic/versions/567e9fe06270_create_new_runs_table_and_remove_legacy_.py @@ -0,0 +1,128 @@ +"""create new runs table and remove legacy tables + +Revision ID: 567e9fe06270 +Revises: 3d2e9fb40a3c +Create Date: 2025-09-22 15:22:28.651178 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "567e9fe06270" +down_revision: Union[str, None] = "3d2e9fb40a3c" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "runs", + sa.Column("id", sa.String(), nullable=False), + sa.Column("status", sa.String(), nullable=False), + sa.Column("completed_at", sa.DateTime(), nullable=True), + sa.Column("stop_reason", sa.String(), nullable=True), + sa.Column("background", sa.Boolean(), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("request_config", sa.JSON(), nullable=True), + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("callback_url", sa.String(), nullable=True), + sa.Column("callback_sent_at", sa.DateTime(), nullable=True), + sa.Column("callback_status_code", sa.Integer(), nullable=True), + sa.Column("callback_error", sa.String(), nullable=True), + sa.Column("ttft_ns", sa.BigInteger(), nullable=True), + sa.Column("total_duration_ns", sa.BigInteger(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("project_id", sa.String(), nullable=True), + sa.Column("base_template_id", sa.String(), nullable=True), + sa.Column("template_id", sa.String(), nullable=True), + sa.Column("deployment_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["agent_id"], + ["agents.id"], + ), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_runs_agent_id", "runs", ["agent_id"], unique=False) + op.create_index("ix_runs_created_at", "runs", ["created_at", "id"], unique=False) + op.create_index("ix_runs_organization_id", "runs", ["organization_id"], unique=False) + op.drop_index(op.f("ix_agents_runs_agent_id_run_id"), table_name="agents_runs") + op.drop_index(op.f("ix_agents_runs_run_id_agent_id"), table_name="agents_runs") + op.drop_table("agents_runs") + op.drop_table("job_messages") + op.add_column("messages", sa.Column("run_id", sa.String(), nullable=True)) + op.create_foreign_key("fk_messages_run_id", "messages", "runs", ["run_id"], ["id"], ondelete="SET NULL") + op.add_column("step_metrics", sa.Column("run_id", sa.String(), nullable=True)) + op.drop_constraint(op.f("step_metrics_job_id_fkey"), "step_metrics", type_="foreignkey") + op.create_foreign_key("fk_step_metrics_run_id", "step_metrics", "runs", ["run_id"], ["id"], ondelete="SET NULL") + op.drop_column("step_metrics", "job_id") + op.add_column("steps", sa.Column("run_id", sa.String(), nullable=True)) + op.drop_index(op.f("ix_steps_job_id"), table_name="steps") + op.create_index("ix_steps_run_id", "steps", ["run_id"], unique=False) + op.drop_constraint(op.f("fk_steps_job_id"), "steps", type_="foreignkey") + op.create_foreign_key("fk_steps_run_id", "steps", "runs", ["run_id"], ["id"], ondelete="SET NULL") + op.drop_column("steps", "job_id") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("steps", sa.Column("job_id", sa.VARCHAR(), autoincrement=False, nullable=True)) + op.drop_constraint("fk_steps_run_id", "steps", type_="foreignkey") + op.create_foreign_key(op.f("fk_steps_job_id"), "steps", "jobs", ["job_id"], ["id"], ondelete="SET NULL") + op.drop_index("ix_steps_run_id", table_name="steps") + op.create_index(op.f("ix_steps_job_id"), "steps", ["job_id"], unique=False) + op.drop_column("steps", "run_id") + op.add_column("step_metrics", sa.Column("job_id", sa.VARCHAR(), autoincrement=False, nullable=True)) + op.drop_constraint("fk_step_metrics_run_id", "step_metrics", type_="foreignkey") + op.create_foreign_key(op.f("step_metrics_job_id_fkey"), "step_metrics", "jobs", ["job_id"], ["id"], ondelete="SET NULL") + op.drop_column("step_metrics", "run_id") + op.drop_constraint("fk_messages_run_id", "messages", type_="foreignkey") + op.drop_column("messages", "run_id") + op.create_table( + "job_messages", + sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column("job_id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("message_id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("created_at", postgresql.TIMESTAMP(timezone=True), server_default=sa.text("now()"), autoincrement=False, nullable=True), + sa.Column("updated_at", postgresql.TIMESTAMP(timezone=True), server_default=sa.text("now()"), autoincrement=False, nullable=True), + sa.Column("is_deleted", sa.BOOLEAN(), server_default=sa.text("false"), autoincrement=False, nullable=False), + sa.Column("_created_by_id", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("_last_updated_by_id", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(["job_id"], ["jobs.id"], name=op.f("fk_job_messages_job_id"), ondelete="CASCADE"), + sa.ForeignKeyConstraint(["message_id"], ["messages.id"], name=op.f("fk_job_messages_message_id"), ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id", name=op.f("pk_job_messages")), + sa.UniqueConstraint( + "job_id", "message_id", name=op.f("unique_job_message"), postgresql_include=[], postgresql_nulls_not_distinct=False + ), + ) + op.create_table( + "agents_runs", + sa.Column("agent_id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("run_id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], name=op.f("agents_runs_agent_id_fkey")), + sa.ForeignKeyConstraint(["run_id"], ["jobs.id"], name=op.f("agents_runs_run_id_fkey")), + sa.PrimaryKeyConstraint("agent_id", "run_id", name=op.f("unique_agent_run")), + ) + op.create_index(op.f("ix_agents_runs_run_id_agent_id"), "agents_runs", ["run_id", "agent_id"], unique=False) + op.create_index(op.f("ix_agents_runs_agent_id_run_id"), "agents_runs", ["agent_id", "run_id"], unique=False) + op.drop_index("ix_runs_organization_id", table_name="runs") + op.drop_index("ix_runs_created_at", table_name="runs") + op.drop_index("ix_runs_agent_id", table_name="runs") + op.drop_table("runs") + # ### end Alembic commands ### diff --git a/alembic/versions/57bcea83af3f_add_various_indexes.py b/alembic/versions/57bcea83af3f_add_various_indexes.py new file mode 100644 index 0000000..14a2f09 --- /dev/null +++ b/alembic/versions/57bcea83af3f_add_various_indexes.py @@ -0,0 +1,41 @@ +"""add various indexes + +Revision ID: 57bcea83af3f +Revises: 5973fd8b8c60 +Create Date: 2025-09-19 10:58:19.658106 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "57bcea83af3f" +down_revision: Union[str, None] = "5973fd8b8c60" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_index("ix_block_hidden", "block", ["hidden"], unique=False) + op.create_index("ix_block_is_template", "block", ["is_template"], unique=False) + op.create_index("ix_block_org_project_template", "block", ["organization_id", "project_id", "is_template"], unique=False) + op.create_index("ix_block_organization_id", "block", ["organization_id"], unique=False) + op.create_index("ix_block_project_id", "block", ["project_id"], unique=False) + op.create_index("ix_jobs_user_id", "jobs", ["user_id"], unique=False) + op.create_index("ix_steps_job_id", "steps", ["job_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_steps_job_id", table_name="steps") + op.drop_index("ix_jobs_user_id", table_name="jobs") + op.drop_index("ix_block_project_id", table_name="block") + op.drop_index("ix_block_organization_id", table_name="block") + op.drop_index("ix_block_org_project_template", table_name="block") + op.drop_index("ix_block_is_template", table_name="block") + op.drop_index("ix_block_hidden", table_name="block") + # ### end Alembic commands ### diff --git a/alembic/versions/5973fd8b8c60_add_agents_runs_table.py b/alembic/versions/5973fd8b8c60_add_agents_runs_table.py new file mode 100644 index 0000000..0519830 --- /dev/null +++ b/alembic/versions/5973fd8b8c60_add_agents_runs_table.py @@ -0,0 +1,51 @@ +"""add agents_runs table + +Revision ID: 5973fd8b8c60 +Revises: eff256d296cb +Create Date: 2025-09-18 10:52:46.270241 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "5973fd8b8c60" +down_revision: Union[str, None] = "eff256d296cb" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "agents_runs", + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("run_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["agent_id"], + ["agents.id"], + ), + sa.ForeignKeyConstraint( + ["run_id"], + ["jobs.id"], + ), + sa.PrimaryKeyConstraint("agent_id", "run_id"), + sa.UniqueConstraint("agent_id", "run_id", name="unique_agent_run"), + ) + op.create_index("ix_agents_runs_agent_id_run_id", "agents_runs", ["agent_id", "run_id"], unique=False) + op.create_index("ix_agents_runs_run_id_agent_id", "agents_runs", ["run_id", "agent_id"], unique=False) + op.add_column("jobs", sa.Column("background", sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("jobs", "background") + op.drop_index("ix_agents_runs_run_id_agent_id", table_name="agents_runs") + op.drop_index("ix_agents_runs_agent_id_run_id", table_name="agents_runs") + op.drop_table("agents_runs") + # ### end Alembic commands ### diff --git a/alembic/versions/5987401b40ae_refactor_agent_memory.py b/alembic/versions/5987401b40ae_refactor_agent_memory.py new file mode 100644 index 0000000..741644e --- /dev/null +++ b/alembic/versions/5987401b40ae_refactor_agent_memory.py @@ -0,0 +1,43 @@ +"""Refactor agent memory + +Revision ID: 5987401b40ae +Revises: 1c8880d671ee +Create Date: 2024-11-25 14:35:00.896507 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "5987401b40ae" +down_revision: Union[str, None] = "1c8880d671ee" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("agents", "tools", new_column_name="tool_names") + op.drop_column("agents", "memory") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("agents", "tool_names", new_column_name="tools") + op.add_column("agents", sa.Column("memory", postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) + # ### end Alembic commands ### diff --git a/alembic/versions/5b804970e6a0_add_hidden_property_to_groups_and_blocks.py b/alembic/versions/5b804970e6a0_add_hidden_property_to_groups_and_blocks.py new file mode 100644 index 0000000..6f97ddd --- /dev/null +++ b/alembic/versions/5b804970e6a0_add_hidden_property_to_groups_and_blocks.py @@ -0,0 +1,35 @@ +"""add_hidden_property_to_groups_and_blocks + +Revision ID: 5b804970e6a0 +Revises: ddb69be34a72 +Create Date: 2025-09-03 22:19:03.825077 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "5b804970e6a0" +down_revision: Union[str, None] = "ddb69be34a72" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add hidden column to groups table + op.add_column("groups", sa.Column("hidden", sa.Boolean(), nullable=True)) + + # Add hidden column to block table + op.add_column("block", sa.Column("hidden", sa.Boolean(), nullable=True)) + + +def downgrade() -> None: + # Remove hidden column from block table + op.drop_column("block", "hidden") + + # Remove hidden column from groups table + op.drop_column("groups", "hidden") diff --git a/alembic/versions/5d27a719b24d_add_organization_id_to_jobs_model.py b/alembic/versions/5d27a719b24d_add_organization_id_to_jobs_model.py new file mode 100644 index 0000000..06ae67e --- /dev/null +++ b/alembic/versions/5d27a719b24d_add_organization_id_to_jobs_model.py @@ -0,0 +1,35 @@ +"""add organization id to jobs model + +Revision ID: 5d27a719b24d +Revises: 18ff61fbc034 +Create Date: 2025-09-10 23:01:45.214589 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "5d27a719b24d" +down_revision: Union[str, None] = "18ff61fbc034" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("jobs", schema=None) as batch_op: + batch_op.add_column(sa.Column("organization_id", sa.String(), nullable=True)) + batch_op.create_foreign_key("fk_jobs_organization_id", "organizations", ["organization_id"], ["id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("jobs", schema=None) as batch_op: + batch_op.drop_constraint("fk_jobs_organization_id", type_="foreignkey") + batch_op.drop_column("organization_id") + # ### end Alembic commands ### diff --git a/alembic/versions/5fb8bba2c373_add_step_metrics.py b/alembic/versions/5fb8bba2c373_add_step_metrics.py new file mode 100644 index 0000000..137b20d --- /dev/null +++ b/alembic/versions/5fb8bba2c373_add_step_metrics.py @@ -0,0 +1,55 @@ +"""add_step_metrics + +Revision ID: 5fb8bba2c373 +Revises: f7f757414d20 +Create Date: 2025-08-07 17:40:11.923402 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "5fb8bba2c373" +down_revision: Union[str, None] = "f7f757414d20" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "step_metrics", + sa.Column("id", sa.String(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=True), + sa.Column("provider_id", sa.String(), nullable=True), + sa.Column("job_id", sa.String(), nullable=True), + sa.Column("llm_request_ns", sa.BigInteger(), nullable=True), + sa.Column("tool_execution_ns", sa.BigInteger(), nullable=True), + sa.Column("step_ns", sa.BigInteger(), nullable=True), + sa.Column("base_template_id", sa.String(), nullable=True), + sa.Column("template_id", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("project_id", sa.String(), nullable=True), + sa.Column("agent_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["id"], ["steps.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["job_id"], ["jobs.id"], ondelete="SET NULL"), + sa.ForeignKeyConstraint(["organization_id"], ["organizations.id"], ondelete="RESTRICT"), + sa.ForeignKeyConstraint(["provider_id"], ["providers.id"], ondelete="RESTRICT"), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("step_metrics") + # ### end Alembic commands ### diff --git a/alembic/versions/60ed28ee7138_add_project_id_to_step_model.py b/alembic/versions/60ed28ee7138_add_project_id_to_step_model.py new file mode 100644 index 0000000..aa0817d --- /dev/null +++ b/alembic/versions/60ed28ee7138_add_project_id_to_step_model.py @@ -0,0 +1,50 @@ +"""add project id to step model + +Revision ID: 60ed28ee7138 +Revises: 46699adc71a7 +Create Date: 2025-07-01 13:12:44.485233 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "60ed28ee7138" +down_revision: Union[str, None] = "46699adc71a7" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("steps", sa.Column("project_id", sa.String(), nullable=True)) + op.execute( + """ + UPDATE steps + SET project_id = agents.project_id + FROM agents + WHERE steps.agent_id = agents.id + AND steps.agent_id IS NOT NULL + AND agents.project_id IS NOT NULL + """ + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("steps", "project_id") + # ### end Alembic commands ### diff --git a/alembic/versions/614c4e53b66e_add_unique_constraint_to_file_id_and_.py b/alembic/versions/614c4e53b66e_add_unique_constraint_to_file_id_and_.py new file mode 100644 index 0000000..8d8813d --- /dev/null +++ b/alembic/versions/614c4e53b66e_add_unique_constraint_to_file_id_and_.py @@ -0,0 +1,38 @@ +"""Add unique constraint to file_id and agent_id on file_agent + +Revision ID: 614c4e53b66e +Revises: 0b496eae90de +Create Date: 2025-06-02 17:03:58.879839 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "614c4e53b66e" +down_revision: Union[str, None] = "0b496eae90de" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint("uq_files_agents_file_agent", "files_agents", ["file_id", "agent_id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("uq_files_agents_file_agent", "files_agents", type_="unique") + # ### end Alembic commands ### diff --git a/alembic/versions/61ee53ec45a5_add_index_on_source_passages_for_files.py b/alembic/versions/61ee53ec45a5_add_index_on_source_passages_for_files.py new file mode 100644 index 0000000..a9ae8a3 --- /dev/null +++ b/alembic/versions/61ee53ec45a5_add_index_on_source_passages_for_files.py @@ -0,0 +1,38 @@ +"""add index on source passages for files + +Revision ID: 61ee53ec45a5 +Revises: 9758adf8fdd3 +Create Date: 2025-06-20 11:10:02.744914 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "61ee53ec45a5" +down_revision: Union[str, None] = "9758adf8fdd3" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_index("source_passages_file_id_idx", "source_passages", ["file_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("source_passages_file_id_idx", table_name="source_passages") + # ### end Alembic commands ### diff --git a/alembic/versions/6756d04c3ddb_add_tools_used_field_to_run_metrics_.py b/alembic/versions/6756d04c3ddb_add_tools_used_field_to_run_metrics_.py new file mode 100644 index 0000000..0a948ab --- /dev/null +++ b/alembic/versions/6756d04c3ddb_add_tools_used_field_to_run_metrics_.py @@ -0,0 +1,31 @@ +"""Add tools_used field to run_metrics table + +Revision ID: 6756d04c3ddb +Revises: e67961ed7c32 +Create Date: 2025-10-17 14:52:53.601368 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "6756d04c3ddb" +down_revision: Union[str, None] = "e67961ed7c32" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("run_metrics", sa.Column("tools_used", sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("run_metrics", "tools_used") + # ### end Alembic commands ### diff --git a/alembic/versions/6c53224a7a58_add_provider_category_to_steps.py b/alembic/versions/6c53224a7a58_add_provider_category_to_steps.py new file mode 100644 index 0000000..bf06a6c --- /dev/null +++ b/alembic/versions/6c53224a7a58_add_provider_category_to_steps.py @@ -0,0 +1,40 @@ +"""add provider category to steps + +Revision ID: 6c53224a7a58 +Revises: cc8dc340836d +Create Date: 2025-05-21 10:09:43.761669 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "6c53224a7a58" +down_revision: Union[str, None] = "cc8dc340836d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("steps", sa.Column("provider_category", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("steps", "provider_category") + # ### end Alembic commands ### diff --git a/alembic/versions/6fbe9cace832_adding_indexes_to_models.py b/alembic/versions/6fbe9cace832_adding_indexes_to_models.py new file mode 100644 index 0000000..5c01f44 --- /dev/null +++ b/alembic/versions/6fbe9cace832_adding_indexes_to_models.py @@ -0,0 +1,52 @@ +"""adding indexes to models + +Revision ID: 6fbe9cace832 +Revises: f895232c144a +Create Date: 2025-01-23 11:02:59.534372 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "6fbe9cace832" +down_revision: Union[str, None] = "f895232c144a" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_index("agent_passages_created_at_id_idx", "agent_passages", ["created_at", "id"], unique=False) + op.create_index("ix_agents_created_at", "agents", ["created_at", "id"], unique=False) + op.create_index("created_at_label_idx", "block", ["created_at", "label"], unique=False) + op.create_index("ix_jobs_created_at", "jobs", ["created_at", "id"], unique=False) + op.create_index("ix_messages_created_at", "messages", ["created_at", "id"], unique=False) + op.create_index("source_passages_created_at_id_idx", "source_passages", ["created_at", "id"], unique=False) + op.create_index("source_created_at_id_idx", "sources", ["created_at", "id"], unique=False) + op.create_index("ix_tools_created_at_name", "tools", ["created_at", "name"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_tools_created_at_name", table_name="tools") + op.drop_index("source_created_at_id_idx", table_name="sources") + op.drop_index("source_passages_created_at_id_idx", table_name="source_passages") + op.drop_index("ix_messages_created_at", table_name="messages") + op.drop_index("ix_jobs_created_at", table_name="jobs") + op.drop_index("created_at_label_idx", table_name="block") + op.drop_index("ix_agents_created_at", table_name="agents") + op.drop_index("agent_passages_created_at_id_idx", table_name="agent_passages") + # ### end Alembic commands ### diff --git a/alembic/versions/6fe79c0525f2_enable_sleeptime_agent_fields.py b/alembic/versions/6fe79c0525f2_enable_sleeptime_agent_fields.py new file mode 100644 index 0000000..8d6cda1 --- /dev/null +++ b/alembic/versions/6fe79c0525f2_enable_sleeptime_agent_fields.py @@ -0,0 +1,42 @@ +"""enable sleeptime agent fields + +Revision ID: 6fe79c0525f2 +Revises: e991d2e3b428 +Create Date: 2025-04-02 08:32:57.412903 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "6fe79c0525f2" +down_revision: Union[str, None] = "e991d2e3b428" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("agents", sa.Column("enable_sleeptime", sa.Boolean(), nullable=True)) + op.alter_column("groups", "background_agents_interval", new_column_name="background_agents_frequency") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("groups", "background_agents_frequency", new_column_name="background_agents_interval") + op.drop_column("agents", "enable_sleeptime") + # ### end Alembic commands ### diff --git a/alembic/versions/74e860718e0d_add_archival_memory_sharing.py b/alembic/versions/74e860718e0d_add_archival_memory_sharing.py new file mode 100644 index 0000000..a63e95e --- /dev/null +++ b/alembic/versions/74e860718e0d_add_archival_memory_sharing.py @@ -0,0 +1,508 @@ +"""add archival memory sharing + +Revision ID: 74e860718e0d +Revises: 15b577c62f3f +Create Date: 2025-07-30 16:15:49.424711 + +""" + +import time +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# Import custom columns if needed +try: + from letta.orm.custom_columns import CommonVector, EmbeddingConfigColumn +except ImportError: + # For environments where these aren't available + EmbeddingConfigColumn = sa.JSON + CommonVector = sa.BLOB + +# revision identifiers, used by Alembic. +revision: str = "74e860718e0d" +down_revision: Union[str, None] = "15b577c62f3f" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # get database connection to check DB type + bind = op.get_bind() + is_sqlite = bind.dialect.name == "sqlite" + + # create new tables with appropriate defaults + if is_sqlite: + op.create_table( + "archives", + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("0"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + else: + # Check if archives table already exists + connection = op.get_bind() + result = connection.execute( + sa.text( + """ + SELECT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = 'archives' + ) + """ + ) + ) + archives_exists = result.scalar() + + if not archives_exists: + op.create_table( + "archives", + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + + op.create_index("ix_archives_created_at", "archives", ["created_at", "id"], unique=False) + op.create_index("ix_archives_organization_id", "archives", ["organization_id"], unique=False) + + if is_sqlite: + op.create_table( + "archives_agents", + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("archive_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("datetime('now')"), nullable=False), + sa.Column("is_owner", sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["archive_id"], ["archives.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("agent_id", "archive_id"), + # TODO: Remove this constraint when we support multiple archives per agent + sa.UniqueConstraint("agent_id", name="unique_agent_archive"), + ) + else: + op.create_table( + "archives_agents", + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("archive_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.Column("is_owner", sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["archive_id"], ["archives.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("agent_id", "archive_id"), + # TODO: Remove this constraint when we support multiple archives per agent + sa.UniqueConstraint("agent_id", name="unique_agent_archive"), + ) + + if is_sqlite: + # For SQLite + # create temporary table to preserve existing agent_passages data + op.execute( + """ + CREATE TEMPORARY TABLE temp_agent_passages AS + SELECT * FROM agent_passages WHERE is_deleted = 0; + """ + ) + + # create default archives and migrate data + # First, create archives for each agent that has passages + op.execute( + """ + INSERT INTO archives (id, name, description, organization_id, created_at, updated_at, is_deleted) + SELECT DISTINCT + 'archive-' || lower(hex(randomblob(16))), + COALESCE(a.name, 'Agent ' || a.id) || '''s Archive', + 'Default archive created during migration', + a.organization_id, + datetime('now'), + datetime('now'), + 0 + FROM temp_agent_passages ap + JOIN agents a ON ap.agent_id = a.id; + """ + ) + + # create archives_agents relationships + op.execute( + """ + INSERT INTO archives_agents (agent_id, archive_id, is_owner, created_at) + SELECT + a.id as agent_id, + ar.id as archive_id, + 1 as is_owner, + datetime('now') as created_at + FROM agents a + JOIN archives ar ON ar.organization_id = a.organization_id + AND ar.name = COALESCE(a.name, 'Agent ' || a.id) || '''s Archive' + WHERE EXISTS ( + SELECT 1 FROM temp_agent_passages ap WHERE ap.agent_id = a.id + ); + """ + ) + + # drop the old agent_passages table + op.drop_index("ix_agent_passages_org_agent", table_name="agent_passages") + op.drop_table("agent_passages") + + # create the new archival_passages table with the new schema + op.create_table( + "archival_passages", + sa.Column("text", sa.String(), nullable=False), + sa.Column("embedding_config", EmbeddingConfigColumn, nullable=False), + sa.Column("metadata_", sa.JSON(), nullable=False), + sa.Column("embedding", CommonVector, nullable=True), # SQLite uses CommonVector for embeddings + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("0"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("archive_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.ForeignKeyConstraint(["archive_id"], ["archives.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + + # migrate data from temp table to archival_passages with archive_id + op.execute( + """ + INSERT INTO archival_passages ( + id, text, embedding_config, metadata_, embedding, + created_at, updated_at, is_deleted, + _created_by_id, _last_updated_by_id, + organization_id, archive_id + ) + SELECT + ap.id, ap.text, ap.embedding_config, ap.metadata_, ap.embedding, + ap.created_at, ap.updated_at, ap.is_deleted, + ap._created_by_id, ap._last_updated_by_id, + ap.organization_id, ar.id as archive_id + FROM temp_agent_passages ap + JOIN agents a ON ap.agent_id = a.id + JOIN archives ar ON ar.organization_id = a.organization_id + AND ar.name = COALESCE(a.name, 'Agent ' || a.id) || '''s Archive'; + """ + ) + + # drop temporary table + op.execute("DROP TABLE temp_agent_passages;") + + # create indexes + op.create_index("ix_archival_passages_archive_id", "archival_passages", ["archive_id"]) + op.create_index("ix_archival_passages_org_archive", "archival_passages", ["organization_id", "archive_id"]) + op.create_index("archival_passages_created_at_id_idx", "archival_passages", ["created_at", "id"]) + + else: + # PostgreSQL + # add archive_id to agent_passages + op.add_column("agent_passages", sa.Column("archive_id", sa.String(), nullable=True)) + + # get connection for batch processing + connection = op.get_bind() + + # get total count of agents with passages + total_agents_result = connection.execute( + sa.text( + """ + SELECT COUNT(DISTINCT a.id) + FROM agent_passages ap + JOIN agents a ON ap.agent_id = a.id + WHERE ap.is_deleted = FALSE + """ + ) + ) + total_agents = total_agents_result.scalar() + + if total_agents > 0: + print(f"Starting archival memory migration for {total_agents} agents...") + start_time = time.time() + + batch_size = 1000 + + # process agents one by one to maintain proper relationships + offset = 0 + while offset < total_agents: + # Get batch of agents that need archives + batch_result = connection.execute( + sa.text( + """ + SELECT DISTINCT a.id, a.name, a.organization_id + FROM agent_passages ap + JOIN agents a ON ap.agent_id = a.id + WHERE ap.is_deleted = FALSE + AND NOT EXISTS ( + SELECT 1 FROM archives_agents aa + WHERE aa.agent_id = a.id + ) + ORDER BY a.id + LIMIT :batch_size + """ + ).bindparams(batch_size=batch_size) + ) + + agents_batch = batch_result.fetchall() + if not agents_batch: + break # No more agents to process + + batch_count = len(agents_batch) + print(f"Processing batch of {batch_count} agents (offset: {offset})...") + + # Create archive and relationship for each agent + for agent_id, agent_name, org_id in agents_batch: + try: + # Create archive + archive_result = connection.execute( + sa.text( + """ + INSERT INTO archives (id, name, description, organization_id, created_at) + VALUES ( + 'archive-' || gen_random_uuid(), + :archive_name, + 'Default archive created during migration', + :org_id, + NOW() + ) + RETURNING id + """ + ).bindparams(archive_name=f"{agent_name or f'Agent {agent_id}'}'s Archive", org_id=org_id) + ) + archive_id = archive_result.scalar() + + # Create agent-archive relationship + connection.execute( + sa.text( + """ + INSERT INTO archives_agents (agent_id, archive_id, is_owner, created_at) + VALUES (:agent_id, :archive_id, TRUE, NOW()) + """ + ).bindparams(agent_id=agent_id, archive_id=archive_id) + ) + except Exception as e: + print(f"Warning: Failed to create archive for agent {agent_id}: {e}") + # Continue with other agents + + offset += batch_count + + print("Archive creation completed. Starting archive_id updates...") + + # update agent_passages with archive_id in batches + total_passages_result = connection.execute( + sa.text( + """ + SELECT COUNT(*) + FROM agent_passages ap + WHERE ap.archive_id IS NULL + AND ap.is_deleted = FALSE + """ + ) + ) + total_passages = total_passages_result.scalar() + + if total_passages > 0: + print(f"Updating archive_id for {total_passages} passages...") + + updated_passages = 0 + update_batch_size = 5000 # larger batch size for updates + + while updated_passages < total_passages: + print( + f"Updating passages {updated_passages + 1} to {min(updated_passages + update_batch_size, total_passages)} of {total_passages}..." + ) + + # Use connection.execute instead of op.execute to get rowcount + result = connection.execute( + sa.text( + """ + UPDATE agent_passages ap + SET archive_id = aa.archive_id + FROM archives_agents aa + WHERE ap.agent_id = aa.agent_id + AND ap.archive_id IS NULL + AND ap.is_deleted = FALSE + AND ap.id IN ( + SELECT id FROM agent_passages + WHERE archive_id IS NULL + AND is_deleted = FALSE + LIMIT :batch_size + ) + """ + ).bindparams(batch_size=update_batch_size) + ) + + rows_updated = result.rowcount + if rows_updated == 0: + break # no more rows to update + + updated_passages += rows_updated + + print(f"Archive_id update completed. Updated {updated_passages} passages.") + + elapsed_time = time.time() - start_time + print(f"Data migration completed successfully in {elapsed_time:.2f} seconds.") + else: + print("No agents with passages found. Skipping data migration.") + + # schema changes + op.alter_column("agent_passages", "archive_id", nullable=False) + op.create_foreign_key("agent_passages_archive_id_fkey", "agent_passages", "archives", ["archive_id"], ["id"], ondelete="CASCADE") + + # drop old indexes and constraints + op.drop_index("ix_agent_passages_org_agent", table_name="agent_passages") + op.drop_index("agent_passages_org_idx", table_name="agent_passages") + op.drop_index("agent_passages_created_at_id_idx", table_name="agent_passages") + op.drop_constraint("agent_passages_agent_id_fkey", "agent_passages", type_="foreignkey") + op.drop_column("agent_passages", "agent_id") + + # rename table and create new indexes + op.rename_table("agent_passages", "archival_passages") + op.create_index("ix_archival_passages_archive_id", "archival_passages", ["archive_id"]) + op.create_index("ix_archival_passages_org_archive", "archival_passages", ["organization_id", "archive_id"]) + op.create_index("archival_passages_org_idx", "archival_passages", ["organization_id"]) + op.create_index("archival_passages_created_at_id_idx", "archival_passages", ["created_at", "id"]) + + +def downgrade() -> None: + # Get database connection to check DB type + bind = op.get_bind() + is_sqlite = bind.dialect.name == "sqlite" + + if is_sqlite: + # For SQLite, we need to migrate data back carefully + # create temporary table to preserve existing archival_passages data + op.execute( + """ + CREATE TEMPORARY TABLE temp_archival_passages AS + SELECT * FROM archival_passages WHERE is_deleted = 0; + """ + ) + + # drop the archival_passages table and indexes + op.drop_index("ix_archival_passages_org_archive", table_name="archival_passages") + op.drop_index("ix_archival_passages_archive_id", table_name="archival_passages") + op.drop_index("archival_passages_created_at_id_idx", table_name="archival_passages") + op.drop_table("archival_passages") + + # recreate agent_passages with old schema + op.create_table( + "agent_passages", + sa.Column("text", sa.String(), nullable=False), + sa.Column("embedding_config", EmbeddingConfigColumn, nullable=False), + sa.Column("metadata_", sa.JSON(), nullable=False), + sa.Column("embedding", CommonVector, nullable=True), # SQLite uses CommonVector for embeddings + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("0"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + + # restore data from archival_passages back to agent_passages + # use the owner relationship from archives_agents to determine agent_id + op.execute( + """ + INSERT INTO agent_passages ( + id, text, embedding_config, metadata_, embedding, + created_at, updated_at, is_deleted, + _created_by_id, _last_updated_by_id, + organization_id, agent_id + ) + SELECT + ap.id, ap.text, ap.embedding_config, ap.metadata_, ap.embedding, + ap.created_at, ap.updated_at, ap.is_deleted, + ap._created_by_id, ap._last_updated_by_id, + ap.organization_id, aa.agent_id + FROM temp_archival_passages ap + JOIN archives_agents aa ON ap.archive_id = aa.archive_id AND aa.is_owner = 1; + """ + ) + + # drop temporary table + op.execute("DROP TABLE temp_archival_passages;") + + # create original indexes + op.create_index("ix_agent_passages_org_agent", "agent_passages", ["organization_id", "agent_id"]) + op.create_index("agent_passages_org_idx", "agent_passages", ["organization_id"]) + op.create_index("agent_passages_created_at_id_idx", "agent_passages", ["created_at", "id"]) + + # drop new tables for SQLite + op.drop_table("archives_agents") + op.drop_index("ix_archives_organization_id", table_name="archives") + op.drop_index("ix_archives_created_at", table_name="archives") + op.drop_table("archives") + else: + # PostgreSQL: + # rename table back + op.drop_index("ix_archival_passages_org_archive", table_name="archival_passages") + op.drop_index("ix_archival_passages_archive_id", table_name="archival_passages") + op.drop_index("archival_passages_org_idx", table_name="archival_passages") + op.drop_index("archival_passages_created_at_id_idx", table_name="archival_passages") + op.rename_table("archival_passages", "agent_passages") + + # add agent_id column back + op.add_column("agent_passages", sa.Column("agent_id", sa.String(), nullable=True)) + + # restore agent_id from archives_agents (use the owner relationship) + op.execute( + """ + UPDATE agent_passages ap + SET agent_id = aa.agent_id + FROM archives_agents aa + WHERE ap.archive_id = aa.archive_id AND aa.is_owner = TRUE; + """ + ) + + # schema changes + op.alter_column("agent_passages", "agent_id", nullable=False) + op.create_foreign_key("agent_passages_agent_id_fkey", "agent_passages", "agents", ["agent_id"], ["id"], ondelete="CASCADE") + + # drop archive_id column and constraint + op.drop_constraint("agent_passages_archive_id_fkey", "agent_passages", type_="foreignkey") + op.drop_column("agent_passages", "archive_id") + + # restore original indexes + op.create_index("ix_agent_passages_org_agent", "agent_passages", ["organization_id", "agent_id"]) + op.create_index("agent_passages_org_idx", "agent_passages", ["organization_id"]) + op.create_index("agent_passages_created_at_id_idx", "agent_passages", ["created_at", "id"]) + + # drop new tables for PostgreSQL + op.drop_table("archives_agents") + op.drop_index("ix_archives_organization_id", table_name="archives") + op.drop_index("ix_archives_created_at", table_name="archives") + op.drop_table("archives") diff --git a/alembic/versions/74f2ede29317_add_background_group_support.py b/alembic/versions/74f2ede29317_add_background_group_support.py new file mode 100644 index 0000000..3bc9863 --- /dev/null +++ b/alembic/versions/74f2ede29317_add_background_group_support.py @@ -0,0 +1,53 @@ +"""add background group support + +Revision ID: 74f2ede29317 +Revises: bff040379479 +Create Date: 2025-04-01 07:45:31.735977 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "74f2ede29317" +down_revision: Union[str, None] = "bff040379479" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("groups", sa.Column("background_agents_interval", sa.Integer(), nullable=True)) + op.add_column("groups", sa.Column("turns_counter", sa.Integer(), nullable=True)) + op.add_column("groups", sa.Column("last_processed_message_id", sa.String(), nullable=True)) + op.create_table( + "groups_blocks", + sa.Column("group_id", sa.String(), nullable=False), + sa.Column("block_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["block_id"], ["block.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["group_id"], ["groups.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("group_id", "block_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("groups_blocks") + op.drop_column("groups", "last_processed_message_id") + op.drop_column("groups", "turns_counter") + op.drop_column("groups", "background_agents_interval") + # ### end Alembic commands ### diff --git a/alembic/versions/750dd87faa12_add_build_request_latency_to_step_.py b/alembic/versions/750dd87faa12_add_build_request_latency_to_step_.py new file mode 100644 index 0000000..5fee6f1 --- /dev/null +++ b/alembic/versions/750dd87faa12_add_build_request_latency_to_step_.py @@ -0,0 +1,33 @@ +"""add build request latency to step metrics + +Revision ID: 750dd87faa12 +Revises: 5b804970e6a0 +Create Date: 2025-09-06 14:28:32.119084 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "750dd87faa12" +down_revision: Union[str, None] = "5b804970e6a0" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("step_metrics", sa.Column("step_start_ns", sa.BigInteger(), nullable=True)) + op.add_column("step_metrics", sa.Column("llm_request_start_ns", sa.BigInteger(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("step_metrics", "step_start_ns") + op.drop_column("step_metrics", "llm_request_start_ns") + # ### end Alembic commands ### diff --git a/alembic/versions/7778731d15e2_added_jobusagestatistics_table.py b/alembic/versions/7778731d15e2_added_jobusagestatistics_table.py new file mode 100644 index 0000000..66a3068 --- /dev/null +++ b/alembic/versions/7778731d15e2_added_jobusagestatistics_table.py @@ -0,0 +1,62 @@ +"""Added JobUsageStatistics table + +Revision ID: 7778731d15e2 +Revises: 8d70372ad130 +Create Date: 2025-01-09 13:20:25.555740 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "7778731d15e2" +down_revision: Union[str, None] = "8d70372ad130" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Create job_usage_statistics table + op.create_table( + "job_usage_statistics", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("job_id", sa.String(), nullable=False), + sa.Column("step_id", sa.String(), nullable=True), + sa.Column("completion_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False), + sa.Column("prompt_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False), + sa.Column("total_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False), + sa.Column("step_count", sa.Integer(), server_default=sa.text("0"), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint(["job_id"], ["jobs.id"], name="fk_job_usage_statistics_job_id", ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id", name="pk_job_usage_statistics"), + ) + + # Create indexes + op.create_index("ix_job_usage_statistics_created_at", "job_usage_statistics", ["created_at"]) + op.create_index("ix_job_usage_statistics_job_id", "job_usage_statistics", ["job_id"]) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Drop indexes + op.drop_index("ix_job_usage_statistics_created_at", "job_usage_statistics") + op.drop_index("ix_job_usage_statistics_job_id", "job_usage_statistics") + + # Drop table + op.drop_table("job_usage_statistics") diff --git a/alembic/versions/77de976590ae_add_groups_for_multi_agent.py b/alembic/versions/77de976590ae_add_groups_for_multi_agent.py new file mode 100644 index 0000000..6180746 --- /dev/null +++ b/alembic/versions/77de976590ae_add_groups_for_multi_agent.py @@ -0,0 +1,71 @@ +"""add groups for multi agent + +Revision ID: 77de976590ae +Revises: 167491cfb7a8 +Create Date: 2025-03-12 14:01:58.034385 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "77de976590ae" +down_revision: Union[str, None] = "167491cfb7a8" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "groups", + sa.Column("id", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=False), + sa.Column("manager_type", sa.String(), nullable=False), + sa.Column("manager_agent_id", sa.String(), nullable=True), + sa.Column("termination_token", sa.String(), nullable=True), + sa.Column("max_turns", sa.Integer(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["manager_agent_id"], ["agents.id"], ondelete="RESTRICT"), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "groups_agents", + sa.Column("group_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["group_id"], ["groups.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("group_id", "agent_id"), + ) + op.add_column("messages", sa.Column("group_id", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("messages", "group_id") + op.drop_table("groups_agents") + op.drop_table("groups") + # ### end Alembic commands ### diff --git a/alembic/versions/7980d239ea08_add_stateless_option_for_agentstate.py b/alembic/versions/7980d239ea08_add_stateless_option_for_agentstate.py new file mode 100644 index 0000000..0a3a0e6 --- /dev/null +++ b/alembic/versions/7980d239ea08_add_stateless_option_for_agentstate.py @@ -0,0 +1,45 @@ +"""Add message_buffer_autoclear option for AgentState + +Revision ID: 7980d239ea08 +Revises: dfafcf8210ca +Create Date: 2025-02-12 14:02:00.918226 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "7980d239ea08" +down_revision: Union[str, None] = "dfafcf8210ca" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Add the column with a temporary nullable=True so we can backfill + op.add_column("agents", sa.Column("message_buffer_autoclear", sa.Boolean(), nullable=True)) + + # Backfill existing rows to set message_buffer_autoclear to False where it's NULL + op.execute("UPDATE agents SET message_buffer_autoclear = false WHERE message_buffer_autoclear IS NULL") + + # Now, enforce nullable=False after backfilling + op.alter_column("agents", "message_buffer_autoclear", nullable=False) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("agents", "message_buffer_autoclear") + # ### end Alembic commands ### diff --git a/alembic/versions/7b189006c97d_rename_batch_id_to_llm_batch_id_on_llm_.py b/alembic/versions/7b189006c97d_rename_batch_id_to_llm_batch_id_on_llm_.py new file mode 100644 index 0000000..b9e2b15 --- /dev/null +++ b/alembic/versions/7b189006c97d_rename_batch_id_to_llm_batch_id_on_llm_.py @@ -0,0 +1,50 @@ +"""Rename batch_id to llm_batch_id on llm_batch_item + +Revision ID: 7b189006c97d +Revises: f2f78d62005c +Create Date: 2025-04-17 16:04:52.045672 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "7b189006c97d" +down_revision: Union[str, None] = "f2f78d62005c" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("llm_batch_items", sa.Column("llm_batch_id", sa.String(), nullable=False)) + op.drop_index("ix_llm_batch_items_batch_id", table_name="llm_batch_items") + op.create_index("ix_llm_batch_items_llm_batch_id", "llm_batch_items", ["llm_batch_id"], unique=False) + op.drop_constraint("llm_batch_items_batch_id_fkey", "llm_batch_items", type_="foreignkey") + op.create_foreign_key(None, "llm_batch_items", "llm_batch_job", ["llm_batch_id"], ["id"], ondelete="CASCADE") + op.drop_column("llm_batch_items", "batch_id") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("llm_batch_items", sa.Column("batch_id", sa.VARCHAR(), autoincrement=False, nullable=False)) + op.drop_constraint(None, "llm_batch_items", type_="foreignkey") + op.create_foreign_key("llm_batch_items_batch_id_fkey", "llm_batch_items", "llm_batch_job", ["batch_id"], ["id"], ondelete="CASCADE") + op.drop_index("ix_llm_batch_items_llm_batch_id", table_name="llm_batch_items") + op.create_index("ix_llm_batch_items_batch_id", "llm_batch_items", ["batch_id"], unique=False) + op.drop_column("llm_batch_items", "llm_batch_id") + # ### end Alembic commands ### diff --git a/alembic/versions/7f652fdd3dba_change_jobmessage_unique_constraint_to_.py b/alembic/versions/7f652fdd3dba_change_jobmessage_unique_constraint_to_.py new file mode 100644 index 0000000..89c9b05 --- /dev/null +++ b/alembic/versions/7f652fdd3dba_change_jobmessage_unique_constraint_to_.py @@ -0,0 +1,42 @@ +"""change JobMessage unique constraint to (job_id,message_id) + +Revision ID: 7f652fdd3dba +Revises: 22a6e413d89c +Create Date: 2025-01-13 14:36:13.626344 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "7f652fdd3dba" +down_revision: Union[str, None] = "22a6e413d89c" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Drop the old unique constraint + op.drop_constraint("uq_job_messages_message_id", "job_messages", type_="unique") + + # Add the new composite unique constraint + op.create_unique_constraint("unique_job_message", "job_messages", ["job_id", "message_id"]) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Drop the new composite constraint + op.drop_constraint("unique_job_message", "job_messages", type_="unique") + + # Restore the old unique constraint + op.create_unique_constraint("uq_job_messages_message_id", "job_messages", ["message_id"]) diff --git a/alembic/versions/7f7933666957_add_stop_reason_to_jobs_table.py b/alembic/versions/7f7933666957_add_stop_reason_to_jobs_table.py new file mode 100644 index 0000000..b138ab2 --- /dev/null +++ b/alembic/versions/7f7933666957_add_stop_reason_to_jobs_table.py @@ -0,0 +1,28 @@ +"""add stop_reason to jobs table + +Revision ID: 7f7933666957 +Revises: d06594144ef3 +Create Date: 2025-09-16 13:20:42.368007 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "7f7933666957" +down_revision: Union[str, None] = "d06594144ef3" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add stop_reason column to jobs table + op.add_column("jobs", sa.Column("stop_reason", sa.String(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("jobs", "stop_reason") diff --git a/alembic/versions/8149a781ac1b_backfill_encrypted_columns_for_.py b/alembic/versions/8149a781ac1b_backfill_encrypted_columns_for_.py new file mode 100644 index 0000000..58e6f6e --- /dev/null +++ b/alembic/versions/8149a781ac1b_backfill_encrypted_columns_for_.py @@ -0,0 +1,349 @@ +"""backfill encrypted columns for providers, mcp, sandbox + +Revision ID: 8149a781ac1b +Revises: 066857381578 +Create Date: 2025-10-13 13:35:55.929562 + +""" + +import os +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy import String, Text +from sqlalchemy.sql import column, table + +from alembic import op +from letta.helpers.crypto_utils import CryptoUtils + +# revision identifiers, used by Alembic. +revision: str = "8149a781ac1b" +down_revision: Union[str, None] = "066857381578" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Check if encryption key is available + encryption_key = os.environ.get("LETTA_ENCRYPTION_KEY") + if not encryption_key: + print("WARNING: LETTA_ENCRYPTION_KEY not set. Skipping data encryption migration.") + print("You can run a separate migration script later to encrypt existing data.") + return + + # Get database connection + connection = op.get_bind() + + # Batch processing configuration + BATCH_SIZE = 1000 # Process 1000 rows at a time + + # Migrate providers data + print("Migrating providers encrypted fields...") + providers = table( + "providers", + column("id", String), + column("api_key", String), + column("api_key_enc", Text), + column("access_key", String), + column("access_key_enc", Text), + ) + + # Count total rows to process + total_count_result = connection.execute( + sa.select(sa.func.count()) + .select_from(providers) + .where( + sa.and_( + sa.or_(providers.c.api_key.isnot(None), providers.c.access_key.isnot(None)), + # Only count rows that need encryption + sa.or_( + sa.and_(providers.c.api_key.isnot(None), providers.c.api_key_enc.is_(None)), + sa.and_(providers.c.access_key.isnot(None), providers.c.access_key_enc.is_(None)), + ), + ) + ) + ).scalar() + + if total_count_result and total_count_result > 0: + print(f"Found {total_count_result} providers records that need encryption") + + encrypted_count = 0 + skipped_count = 0 + offset = 0 + + # Process in batches + while True: + # Select batch of rows + provider_rows = connection.execute( + sa.select( + providers.c.id, + providers.c.api_key, + providers.c.api_key_enc, + providers.c.access_key, + providers.c.access_key_enc, + ) + .where( + sa.and_( + sa.or_(providers.c.api_key.isnot(None), providers.c.access_key.isnot(None)), + # Only select rows that need encryption + sa.or_( + sa.and_(providers.c.api_key.isnot(None), providers.c.api_key_enc.is_(None)), + sa.and_(providers.c.access_key.isnot(None), providers.c.access_key_enc.is_(None)), + ), + ) + ) + .order_by(providers.c.id) # Ensure consistent ordering + .limit(BATCH_SIZE) + .offset(offset) + ).fetchall() + + if not provider_rows: + break # No more rows to process + + # Prepare batch updates + batch_updates = [] + + for row in provider_rows: + updates = {"id": row.id} + has_updates = False + + # Encrypt api_key if present and not already encrypted + if row.api_key and not row.api_key_enc: + try: + updates["api_key_enc"] = CryptoUtils.encrypt(row.api_key, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt api_key for providers id={row.id}: {e}") + elif row.api_key_enc: + skipped_count += 1 + + # Encrypt access_key if present and not already encrypted + if row.access_key and not row.access_key_enc: + try: + updates["access_key_enc"] = CryptoUtils.encrypt(row.access_key, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt access_key for providers id={row.id}: {e}") + elif row.access_key_enc: + skipped_count += 1 + + if has_updates: + batch_updates.append(updates) + encrypted_count += 1 + + # Execute batch update if there are updates + if batch_updates: + # Use bulk update for better performance + for update_data in batch_updates: + row_id = update_data.pop("id") + if update_data: # Only update if there are fields to update + connection.execute(providers.update().where(providers.c.id == row_id).values(**update_data)) + + # Progress indicator for large datasets + if encrypted_count > 0 and encrypted_count % 10000 == 0: + print(f" Progress: Encrypted {encrypted_count} providers records...") + + offset += BATCH_SIZE + + # For very large datasets, commit periodically to avoid long transactions + if encrypted_count > 0 and encrypted_count % 50000 == 0: + connection.commit() + + print(f"providers: Encrypted {encrypted_count} records, skipped {skipped_count} already encrypted fields") + else: + print("providers: No records need encryption") + + # Migrate sandbox_environment_variables data + print("Migrating sandbox_environment_variables encrypted fields...") + sandbox_environment_variables = table( + "sandbox_environment_variables", + column("id", String), + column("value", String), + column("value_enc", Text), + ) + + # Count total rows to process + total_count_result = connection.execute( + sa.select(sa.func.count()) + .select_from(sandbox_environment_variables) + .where( + sa.and_( + sandbox_environment_variables.c.value.isnot(None), + sandbox_environment_variables.c.value_enc.is_(None), + ) + ) + ).scalar() + + if total_count_result and total_count_result > 0: + print(f"Found {total_count_result} sandbox_environment_variables records that need encryption") + + encrypted_count = 0 + skipped_count = 0 + offset = 0 + + # Process in batches + while True: + # Select batch of rows + env_var_rows = connection.execute( + sa.select( + sandbox_environment_variables.c.id, + sandbox_environment_variables.c.value, + sandbox_environment_variables.c.value_enc, + ) + .where( + sa.and_( + sandbox_environment_variables.c.value.isnot(None), + sandbox_environment_variables.c.value_enc.is_(None), + ) + ) + .order_by(sandbox_environment_variables.c.id) # Ensure consistent ordering + .limit(BATCH_SIZE) + .offset(offset) + ).fetchall() + + if not env_var_rows: + break # No more rows to process + + # Prepare batch updates + batch_updates = [] + + for row in env_var_rows: + updates = {"id": row.id} + has_updates = False + + # Encrypt value if present and not already encrypted + if row.value and not row.value_enc: + try: + updates["value_enc"] = CryptoUtils.encrypt(row.value, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt value for sandbox_environment_variables id={row.id}: {e}") + elif row.value_enc: + skipped_count += 1 + + if has_updates: + batch_updates.append(updates) + encrypted_count += 1 + + # Execute batch update if there are updates + if batch_updates: + # Use bulk update for better performance + for update_data in batch_updates: + row_id = update_data.pop("id") + if update_data: # Only update if there are fields to update + connection.execute( + sandbox_environment_variables.update().where(sandbox_environment_variables.c.id == row_id).values(**update_data) + ) + + # Progress indicator for large datasets + if encrypted_count > 0 and encrypted_count % 10000 == 0: + print(f" Progress: Encrypted {encrypted_count} sandbox_environment_variables records...") + + offset += BATCH_SIZE + + # For very large datasets, commit periodically to avoid long transactions + if encrypted_count > 0 and encrypted_count % 50000 == 0: + connection.commit() + + print(f"sandbox_environment_variables: Encrypted {encrypted_count} records, skipped {skipped_count} already encrypted fields") + else: + print("sandbox_environment_variables: No records need encryption") + + # Migrate mcp_oauth data (only authorization_code field) + print("Migrating mcp_oauth encrypted fields...") + mcp_oauth = table( + "mcp_oauth", + column("id", String), + column("authorization_code", Text), + column("authorization_code_enc", Text), + ) + + # Count total rows to process + total_count_result = connection.execute( + sa.select(sa.func.count()) + .select_from(mcp_oauth) + .where( + sa.and_( + mcp_oauth.c.authorization_code.isnot(None), + mcp_oauth.c.authorization_code_enc.is_(None), + ) + ) + ).scalar() + + if total_count_result and total_count_result > 0: + print(f"Found {total_count_result} mcp_oauth records that need encryption") + + encrypted_count = 0 + skipped_count = 0 + offset = 0 + + # Process in batches + while True: + # Select batch of rows + oauth_rows = connection.execute( + sa.select( + mcp_oauth.c.id, + mcp_oauth.c.authorization_code, + mcp_oauth.c.authorization_code_enc, + ) + .where( + sa.and_( + mcp_oauth.c.authorization_code.isnot(None), + mcp_oauth.c.authorization_code_enc.is_(None), + ) + ) + .order_by(mcp_oauth.c.id) # Ensure consistent ordering + .limit(BATCH_SIZE) + .offset(offset) + ).fetchall() + + if not oauth_rows: + break # No more rows to process + + # Prepare batch updates + batch_updates = [] + + for row in oauth_rows: + updates = {"id": row.id} + has_updates = False + + # Encrypt authorization_code if present and not already encrypted + if row.authorization_code and not row.authorization_code_enc: + try: + updates["authorization_code_enc"] = CryptoUtils.encrypt(row.authorization_code, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt authorization_code for mcp_oauth id={row.id}: {e}") + elif row.authorization_code_enc: + skipped_count += 1 + + if has_updates: + batch_updates.append(updates) + encrypted_count += 1 + + # Execute batch update if there are updates + if batch_updates: + # Use bulk update for better performance + for update_data in batch_updates: + row_id = update_data.pop("id") + if update_data: # Only update if there are fields to update + connection.execute(mcp_oauth.update().where(mcp_oauth.c.id == row_id).values(**update_data)) + + # Progress indicator for large datasets + if encrypted_count > 0 and encrypted_count % 10000 == 0: + print(f" Progress: Encrypted {encrypted_count} mcp_oauth records...") + + offset += BATCH_SIZE + + # For very large datasets, commit periodically to avoid long transactions + if encrypted_count > 0 and encrypted_count % 50000 == 0: + connection.commit() + + print(f"mcp_oauth: Encrypted {encrypted_count} records, skipped {skipped_count} already encrypted fields") + else: + print("mcp_oauth: No records need encryption") + print("Migration complete. Plaintext columns are retained for rollback safety.") + + +def downgrade() -> None: + pass diff --git a/alembic/versions/82feb220a9b8_add_source_column_to_provider_traces.py b/alembic/versions/82feb220a9b8_add_source_column_to_provider_traces.py new file mode 100644 index 0000000..5baad52 --- /dev/null +++ b/alembic/versions/82feb220a9b8_add_source_column_to_provider_traces.py @@ -0,0 +1,27 @@ +"""add source column to provider_traces + +Revision ID: 82feb220a9b8 +Revises: 539afa667cff +Create Date: 2026-01-18 21:09:59.529688 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "82feb220a9b8" +down_revision: Union[str, None] = "539afa667cff" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("provider_traces", sa.Column("source", sa.String(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("provider_traces", "source") diff --git a/alembic/versions/878607e41ca4_add_provider_category.py b/alembic/versions/878607e41ca4_add_provider_category.py new file mode 100644 index 0000000..48d0db9 --- /dev/null +++ b/alembic/versions/878607e41ca4_add_provider_category.py @@ -0,0 +1,40 @@ +"""add provider category + +Revision ID: 878607e41ca4 +Revises: 0335b1eb9c40 +Create Date: 2025-05-06 12:10:25.751536 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "878607e41ca4" +down_revision: Union[str, None] = "0335b1eb9c40" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("providers", sa.Column("provider_category", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("providers", "provider_category") + # ### end Alembic commands ### diff --git a/alembic/versions/887a4367b560_convert_stop_reason_from_enum_to_string.py b/alembic/versions/887a4367b560_convert_stop_reason_from_enum_to_string.py new file mode 100644 index 0000000..e330299 --- /dev/null +++ b/alembic/versions/887a4367b560_convert_stop_reason_from_enum_to_string.py @@ -0,0 +1,39 @@ +"""convert_stop_reason_from_enum_to_string + +Revision ID: 887a4367b560 +Revises: d5103ee17ed5 +Create Date: 2025-08-27 16:34:45.605580 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "887a4367b560" +down_revision: Union[str, None] = "d5103ee17ed5" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite it doesn't enforce column types strictly, + # so the existing enum values will continue to work as strings. + if not settings.letta_pg_uri_no_default: + return + + op.execute( + """ + ALTER TABLE steps + ALTER COLUMN stop_reason TYPE VARCHAR + USING stop_reason::VARCHAR + """ + ) + + +def downgrade() -> None: + # This is a one-way migration as we can't easily recreate the enum type + # If needed, you would need to create the enum type and cast back + pass diff --git a/alembic/versions/88f9432739a9_add_jobtype_to_job_table.py b/alembic/versions/88f9432739a9_add_jobtype_to_job_table.py new file mode 100644 index 0000000..a097c3a --- /dev/null +++ b/alembic/versions/88f9432739a9_add_jobtype_to_job_table.py @@ -0,0 +1,44 @@ +"""add JobType to Job table + +Revision ID: 88f9432739a9 +Revises: 7778731d15e2 +Create Date: 2025-01-10 13:46:44.089110 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "88f9432739a9" +down_revision: Union[str, None] = "7778731d15e2" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Add job_type column with default value + op.add_column("jobs", sa.Column("job_type", sa.String(), nullable=True)) + + # Set existing rows to have the default value of JobType.JOB + op.execute("UPDATE jobs SET job_type = 'job' WHERE job_type IS NULL") + + # Make the column non-nullable after setting default values + op.alter_column("jobs", "job_type", existing_type=sa.String(), nullable=False) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Remove the job_type column + op.drop_column("jobs", "job_type") diff --git a/alembic/versions/89b595051e48_replace_composite_runs_index.py b/alembic/versions/89b595051e48_replace_composite_runs_index.py new file mode 100644 index 0000000..ea5494d --- /dev/null +++ b/alembic/versions/89b595051e48_replace_composite_runs_index.py @@ -0,0 +1,31 @@ +"""replace composite runs index + +Revision ID: 89b595051e48 +Revises: f9ad1c25fd2b +Create Date: 2025-10-06 13:17:09.918439 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "89b595051e48" +down_revision: Union[str, None] = "f9ad1c25fd2b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_messages_run_err_sequence"), table_name="messages") + op.create_index("ix_messages_run_sequence", "messages", ["run_id", "sequence_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_messages_run_sequence", table_name="messages") + op.create_index(op.f("ix_messages_run_err_sequence"), "messages", ["run_id", "is_err", "sequence_id"], unique=False) + # ### end Alembic commands ### diff --git a/alembic/versions/89fd4648866b_add_last_stop_reason_to_agent_state.py b/alembic/versions/89fd4648866b_add_last_stop_reason_to_agent_state.py new file mode 100644 index 0000000..c8de5e1 --- /dev/null +++ b/alembic/versions/89fd4648866b_add_last_stop_reason_to_agent_state.py @@ -0,0 +1,31 @@ +"""add last_stop_reason to agent state + +Revision ID: 89fd4648866b +Revises: f6cd5a1e519d +Create Date: 2025-10-27 16:55:54.383688 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "89fd4648866b" +down_revision: Union[str, None] = "f6cd5a1e519d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("agents", sa.Column("last_stop_reason", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("agents", "last_stop_reason") + # ### end Alembic commands ### diff --git a/alembic/versions/8d70372ad130_adding_jobmessages_table.py b/alembic/versions/8d70372ad130_adding_jobmessages_table.py new file mode 100644 index 0000000..2c9c0a5 --- /dev/null +++ b/alembic/versions/8d70372ad130_adding_jobmessages_table.py @@ -0,0 +1,56 @@ +"""adding JobMessages table + +Revision ID: 8d70372ad130 +Revises: cdb3db091113 +Create Date: 2025-01-08 17:57:20.325596 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "8d70372ad130" +down_revision: Union[str, None] = "cdb3db091113" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.create_table( + "job_messages", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("job_id", sa.String(), nullable=False), + sa.Column("message_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint(["job_id"], ["jobs.id"], name="fk_job_messages_job_id", ondelete="CASCADE"), + sa.ForeignKeyConstraint(["message_id"], ["messages.id"], name="fk_job_messages_message_id", ondelete="CASCADE", use_alter=True), + sa.PrimaryKeyConstraint("id", name="pk_job_messages"), + sa.UniqueConstraint("message_id", name="uq_job_messages_message_id"), + ) + + # Add indexes + op.create_index("ix_job_messages_job_id", "job_messages", ["job_id"], unique=False) + op.create_index("ix_job_messages_created_at", "job_messages", ["created_at"], unique=False) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.drop_index("ix_job_messages_created_at", "job_messages") + op.drop_index("ix_job_messages_job_id", "job_messages") + op.drop_table("job_messages") diff --git a/alembic/versions/90bb156e71df_rename_sleeptime_agent_frequency.py b/alembic/versions/90bb156e71df_rename_sleeptime_agent_frequency.py new file mode 100644 index 0000000..43b7f5f --- /dev/null +++ b/alembic/versions/90bb156e71df_rename_sleeptime_agent_frequency.py @@ -0,0 +1,38 @@ +"""rename sleeptime_agent_frequency + +Revision ID: 90bb156e71df +Revises: 6fe79c0525f2 +Create Date: 2025-04-03 17:20:26.218596 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "90bb156e71df" +down_revision: Union[str, None] = "6fe79c0525f2" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("groups", "background_agents_frequency", new_column_name="sleeptime_agent_frequency") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("groups", "sleeptime_agent_frequency", new_column_name="background_agents_frequency") + # ### end Alembic commands ### diff --git a/alembic/versions/90fd814d0cda_add_callback_error_field_to_jobs.py b/alembic/versions/90fd814d0cda_add_callback_error_field_to_jobs.py new file mode 100644 index 0000000..dba8736 --- /dev/null +++ b/alembic/versions/90fd814d0cda_add_callback_error_field_to_jobs.py @@ -0,0 +1,40 @@ +"""Add callback error field to jobs + +Revision ID: 90fd814d0cda +Revises: c0ef3ff26306 +Create Date: 2025-06-16 13:04:53.496195 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "90fd814d0cda" +down_revision: Union[str, None] = "c0ef3ff26306" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("jobs", sa.Column("callback_error", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("jobs", "callback_error") + # ### end Alembic commands ### diff --git a/alembic/versions/915b68780108_add_providers_data_to_orm.py b/alembic/versions/915b68780108_add_providers_data_to_orm.py new file mode 100644 index 0000000..3db4dd6 --- /dev/null +++ b/alembic/versions/915b68780108_add_providers_data_to_orm.py @@ -0,0 +1,56 @@ +"""Add providers data to ORM + +Revision ID: 915b68780108 +Revises: 400501b04bf0 +Create Date: 2025-01-07 10:49:04.717058 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "915b68780108" +down_revision: Union[str, None] = "400501b04bf0" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "providers", + sa.Column("name", sa.String(), nullable=False), + sa.Column("api_key", sa.String(), nullable=True), + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("providers") + # ### end Alembic commands ### diff --git a/alembic/versions/9275f62ad282_add_v2_protocol_fields_to_provider_traces.py b/alembic/versions/9275f62ad282_add_v2_protocol_fields_to_provider_traces.py new file mode 100644 index 0000000..97fa2f7 --- /dev/null +++ b/alembic/versions/9275f62ad282_add_v2_protocol_fields_to_provider_traces.py @@ -0,0 +1,32 @@ +"""Add v2 protocol fields to provider_traces + +Revision ID: 9275f62ad282 +Revises: 297e8217e952 +Create Date: 2026-01-22 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +revision: str = "9275f62ad282" +down_revision: Union[str, None] = "297e8217e952" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("provider_traces", sa.Column("org_id", sa.String(), nullable=True)) + op.add_column("provider_traces", sa.Column("user_id", sa.String(), nullable=True)) + op.add_column("provider_traces", sa.Column("compaction_settings", sa.JSON(), nullable=True)) + op.add_column("provider_traces", sa.Column("llm_config", sa.JSON(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("provider_traces", "llm_config") + op.drop_column("provider_traces", "compaction_settings") + op.drop_column("provider_traces", "user_id") + op.drop_column("provider_traces", "org_id") diff --git a/alembic/versions/9556081ce65b_add_bedrock_creds_to_byok.py b/alembic/versions/9556081ce65b_add_bedrock_creds_to_byok.py new file mode 100644 index 0000000..77430d9 --- /dev/null +++ b/alembic/versions/9556081ce65b_add_bedrock_creds_to_byok.py @@ -0,0 +1,42 @@ +"""add bedrock creds to byok + +Revision ID: 9556081ce65b +Revises: 90fd814d0cda +Create Date: 2025-06-18 11:15:39.461916 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "9556081ce65b" +down_revision: Union[str, None] = "90fd814d0cda" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("providers", sa.Column("access_key", sa.String(), nullable=True)) + op.add_column("providers", sa.Column("region", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("providers", "region") + op.drop_column("providers", "access_key") + # ### end Alembic commands ### diff --git a/alembic/versions/95badb46fdf9_migrate_messages_to_the_orm.py b/alembic/versions/95badb46fdf9_migrate_messages_to_the_orm.py new file mode 100644 index 0000000..c84730d --- /dev/null +++ b/alembic/versions/95badb46fdf9_migrate_messages_to_the_orm.py @@ -0,0 +1,72 @@ +"""Migrate message to orm + +Revision ID: 95badb46fdf9 +Revises: 3c683a662c82 +Create Date: 2024-12-05 14:02:04.163150 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "95badb46fdf9" +down_revision: Union[str, None] = "08b2f8225812" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("messages", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True)) + op.add_column("messages", sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False)) + op.add_column("messages", sa.Column("_created_by_id", sa.String(), nullable=True)) + op.add_column("messages", sa.Column("_last_updated_by_id", sa.String(), nullable=True)) + op.add_column("messages", sa.Column("organization_id", sa.String(), nullable=True)) + # Populate `organization_id` based on `user_id` + # Use a raw SQL query to update the organization_id + op.execute( + """ + UPDATE messages + SET organization_id = users.organization_id + FROM users + WHERE messages.user_id = users.id + """ + ) + op.alter_column("messages", "organization_id", nullable=False) + op.alter_column("messages", "tool_calls", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=False) + op.alter_column("messages", "created_at", existing_type=postgresql.TIMESTAMP(timezone=True), nullable=False) + op.drop_index("message_idx_user", table_name="messages") + op.create_foreign_key(None, "messages", "agents", ["agent_id"], ["id"]) + op.create_foreign_key(None, "messages", "organizations", ["organization_id"], ["id"]) + op.drop_column("messages", "user_id") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("messages", sa.Column("user_id", sa.VARCHAR(), autoincrement=False, nullable=False)) + op.drop_constraint(None, "messages", type_="foreignkey") + op.drop_constraint(None, "messages", type_="foreignkey") + op.create_index("message_idx_user", "messages", ["user_id", "agent_id"], unique=False) + op.alter_column("messages", "created_at", existing_type=postgresql.TIMESTAMP(timezone=True), nullable=True) + op.alter_column("messages", "tool_calls", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=True) + op.drop_column("messages", "organization_id") + op.drop_column("messages", "_last_updated_by_id") + op.drop_column("messages", "_created_by_id") + op.drop_column("messages", "is_deleted") + op.drop_column("messages", "updated_at") + # ### end Alembic commands ### diff --git a/alembic/versions/9758adf8fdd3_add_run_completion_and_duration_to_.py b/alembic/versions/9758adf8fdd3_add_run_completion_and_duration_to_.py new file mode 100644 index 0000000..529e407 --- /dev/null +++ b/alembic/versions/9758adf8fdd3_add_run_completion_and_duration_to_.py @@ -0,0 +1,42 @@ +"""add_run_completion_and_duration_to_agents_table + +Revision ID: 9758adf8fdd3 +Revises: 9556081ce65b +Create Date: 2025-06-18 18:22:31.135685 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "9758adf8fdd3" +down_revision: Union[str, None] = "9556081ce65b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("agents", sa.Column("last_run_completion", sa.DateTime(timezone=True), nullable=True)) + op.add_column("agents", sa.Column("last_run_duration_ms", sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("agents", "last_run_duration_ms") + op.drop_column("agents", "last_run_completion") + # ### end Alembic commands ### diff --git a/alembic/versions/9792f94e961d_add_file_processing_status_to_.py b/alembic/versions/9792f94e961d_add_file_processing_status_to_.py new file mode 100644 index 0000000..52859bf --- /dev/null +++ b/alembic/versions/9792f94e961d_add_file_processing_status_to_.py @@ -0,0 +1,59 @@ +"""Add file processing status to FileMetadata and related indices + +Revision ID: 9792f94e961d +Revises: cdd4a1c11aee +Create Date: 2025-06-05 18:51:57.022594 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "9792f94e961d" +down_revision: Union[str, None] = "cdd4a1c11aee" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Step 1: Create constraint + op.create_unique_constraint("uq_file_contents_file_id", "file_contents", ["file_id"]) + + # Step 2: Add processing_status as nullable first + op.add_column("files", sa.Column("processing_status", sa.String(), nullable=True)) + op.add_column("files", sa.Column("error_message", sa.Text(), nullable=True)) + + # Step 3: Backfill existing rows with 'completed' + op.execute("UPDATE files SET processing_status = 'completed'") + + # Step 4: Make the column non-nullable now that it's backfilled + op.alter_column("files", "processing_status", nullable=False) + + # Step 5: Create indices + op.create_index("ix_files_org_created", "files", ["organization_id", sa.literal_column("created_at DESC")], unique=False) + op.create_index("ix_files_processing_status", "files", ["processing_status"], unique=False) + op.create_index("ix_files_source_created", "files", ["source_id", sa.literal_column("created_at DESC")], unique=False) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_files_source_created", table_name="files") + op.drop_index("ix_files_processing_status", table_name="files") + op.drop_index("ix_files_org_created", table_name="files") + op.drop_column("files", "error_message") + op.drop_column("files", "processing_status") + op.drop_constraint("uq_file_contents_file_id", "file_contents", type_="unique") + # ### end Alembic commands ### diff --git a/alembic/versions/9a505cc7eca9_create_a_baseline_migrations.py b/alembic/versions/9a505cc7eca9_create_a_baseline_migrations.py new file mode 100644 index 0000000..a9fb0be --- /dev/null +++ b/alembic/versions/9a505cc7eca9_create_a_baseline_migrations.py @@ -0,0 +1,205 @@ +"""Create a baseline migrations + +Revision ID: 9a505cc7eca9 +Revises: +Create Date: 2024-10-11 14:19:19.875656 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +import letta.orm +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "9a505cc7eca9" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + import pgvector + + op.create_table( + "agent_source_mapping", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("source_id", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("agent_source_mapping_idx_user", "agent_source_mapping", ["user_id", "agent_id", "source_id"], unique=False) + op.create_table( + "agents", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("message_ids", sa.JSON(), nullable=True), + sa.Column("memory", sa.JSON(), nullable=True), + sa.Column("system", sa.String(), nullable=True), + sa.Column("agent_type", sa.String(), nullable=True), + sa.Column("llm_config", letta.orm.custom_columns.LLMConfigColumn(), nullable=True), + sa.Column("embedding_config", letta.orm.custom_columns.EmbeddingConfigColumn(), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("tools", sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("agents_idx_user", "agents", ["user_id"], unique=False) + op.create_table( + "block", + sa.Column("id", sa.String(), nullable=False), + sa.Column("value", sa.String(), nullable=False), + sa.Column("limit", sa.BIGINT(), nullable=True), + sa.Column("name", sa.String(), nullable=True), + sa.Column("template", sa.Boolean(), nullable=True), + sa.Column("label", sa.String(), nullable=False), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("user_id", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("block_idx_user", "block", ["user_id"], unique=False) + op.create_table( + "files", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=False), + sa.Column("source_id", sa.String(), nullable=False), + sa.Column("file_name", sa.String(), nullable=True), + sa.Column("file_path", sa.String(), nullable=True), + sa.Column("file_type", sa.String(), nullable=True), + sa.Column("file_size", sa.Integer(), nullable=True), + sa.Column("file_creation_date", sa.String(), nullable=True), + sa.Column("file_last_modified_date", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "jobs", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=True), + sa.Column("status", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "messages", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=False), + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("role", sa.String(), nullable=False), + sa.Column("text", sa.String(), nullable=True), + sa.Column("model", sa.String(), nullable=True), + sa.Column("name", sa.String(), nullable=True), + sa.Column("tool_calls", letta.orm.message.ToolCallColumn(), nullable=True), + sa.Column("tool_call_id", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("message_idx_user", "messages", ["user_id", "agent_id"], unique=False) + op.create_table( + "organizations", + sa.Column("id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("created_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint("id", name="organizations_pkey"), + ) + op.create_table( + "passages", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=False), + sa.Column("text", sa.String(), nullable=True), + sa.Column("file_id", sa.String(), nullable=True), + sa.Column("agent_id", sa.String(), nullable=True), + sa.Column("source_id", sa.String(), nullable=True), + sa.Column("embedding", pgvector.sqlalchemy.Vector(dim=4096), nullable=True), + sa.Column("embedding_config", letta.orm.custom_columns.EmbeddingConfigColumn(), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("passage_idx_user", "passages", ["user_id", "agent_id", "file_id"], unique=False) + op.create_table( + "sources", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("embedding_config", letta.orm.custom_columns.EmbeddingConfigColumn(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("sources_idx_user", "sources", ["user_id"], unique=False) + op.create_table( + "tokens", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=False), + sa.Column("key", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("tokens_idx_key", "tokens", ["key"], unique=False) + op.create_index("tokens_idx_user", "tokens", ["user_id"], unique=False) + + op.create_table( + "users", + sa.Column("id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("org_id", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("created_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), + sa.Column("policies_accepted", sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint("id", name="users_pkey"), + ) + op.create_table( + "tools", + sa.Column("id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("user_id", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("description", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("source_type", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("source_code", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("json_schema", postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), + sa.Column("module", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("tags", postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint("id", name="tools_pkey"), + ) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.drop_table("users") + op.drop_table("tools") + op.drop_index("tokens_idx_user", table_name="tokens") + op.drop_index("tokens_idx_key", table_name="tokens") + op.drop_table("tokens") + op.drop_index("sources_idx_user", table_name="sources") + op.drop_table("sources") + op.drop_index("passage_idx_user", table_name="passages") + op.drop_table("passages") + op.drop_table("organizations") + op.drop_index("message_idx_user", table_name="messages") + op.drop_table("messages") + op.drop_table("jobs") + op.drop_table("files") + op.drop_index("block_idx_user", table_name="block") + op.drop_table("block") + op.drop_index("agents_idx_user", table_name="agents") + op.drop_table("agents") + op.drop_index("agent_source_mapping_idx_user", table_name="agent_source_mapping") + op.drop_table("agent_source_mapping") diff --git a/alembic/versions/9ecbdbaa409f_add_table_to_store_mcp_servers.py b/alembic/versions/9ecbdbaa409f_add_table_to_store_mcp_servers.py new file mode 100644 index 0000000..eb2a148 --- /dev/null +++ b/alembic/versions/9ecbdbaa409f_add_table_to_store_mcp_servers.py @@ -0,0 +1,60 @@ +"""add table to store mcp servers + +Revision ID: 9ecbdbaa409f +Revises: 6c53224a7a58 +Create Date: 2025-05-21 15:25:12.483026 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +import letta +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "9ecbdbaa409f" +down_revision: Union[str, None] = "6c53224a7a58" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "mcp_server", + sa.Column("id", sa.String(), nullable=False), + sa.Column("server_name", sa.String(), nullable=False), + sa.Column("server_type", sa.String(), nullable=False), + sa.Column("server_url", sa.String(), nullable=True), + sa.Column("stdio_config", letta.orm.custom_columns.MCPStdioServerConfigColumn(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.UniqueConstraint("server_name", "organization_id", name="uix_name_organization_mcp_server"), + ) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("mcp_server") + # ### end Alembic commands ### diff --git a/alembic/versions/9fa274fb0b83_backfill_hidden_for_subagent_role_tag.py b/alembic/versions/9fa274fb0b83_backfill_hidden_for_subagent_role_tag.py new file mode 100644 index 0000000..3a1dfc7 --- /dev/null +++ b/alembic/versions/9fa274fb0b83_backfill_hidden_for_subagent_role_tag.py @@ -0,0 +1,35 @@ +"""backfill hidden for role:subagent agents + +Revision ID: 9fa274fb0b83 +Revises: 45402909a46b +Create Date: 2026-03-18 16:46:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "9fa274fb0b83" +down_revision: Union[str, None] = "45402909a46b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute( + """ + UPDATE agents a + SET hidden = true + FROM agents_tags at + WHERE (a.hidden IS NULL OR a.hidden = false) + AND a.id = at.agent_id + AND at.tag = 'role:subagent' + """ + ) + + +def downgrade() -> None: + # Data-only backfill; no safe automatic rollback. + pass diff --git a/alembic/versions/a08c972e781b_add_index_on_agents_organization_id_and_.py b/alembic/versions/a08c972e781b_add_index_on_agents_organization_id_and_.py new file mode 100644 index 0000000..68af301 --- /dev/null +++ b/alembic/versions/a08c972e781b_add_index_on_agents_organization_id_and_.py @@ -0,0 +1,40 @@ +"""add index on agents organization_id and created_by_id + +Revision ID: a08c972e781b +Revises: 9fa274fb0b83 +Create Date: 2026-03-22 16:16:30.094739 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "a08c972e781b" +down_revision: Union[str, None] = "9fa274fb0b83" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + connection = op.get_bind() + connection.commit() + autocommit_connection = connection.execution_options(isolation_level="AUTOCOMMIT") + autocommit_connection.exec_driver_sql( + """ + CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_agents_organization_id_created_by_id + ON agents USING btree (organization_id, _created_by_id) + """ + ) + + +def downgrade() -> None: + connection = op.get_bind() + connection.commit() + autocommit_connection = connection.execution_options(isolation_level="AUTOCOMMIT") + autocommit_connection.exec_driver_sql( + """ + DROP INDEX CONCURRENTLY IF EXISTS ix_agents_organization_id_created_by_id + """ + ) diff --git a/alembic/versions/a113caac453e_add_identities_table.py b/alembic/versions/a113caac453e_add_identities_table.py new file mode 100644 index 0000000..8d83aaf --- /dev/null +++ b/alembic/versions/a113caac453e_add_identities_table.py @@ -0,0 +1,75 @@ +"""add identities table + +Revision ID: a113caac453e +Revises: 7980d239ea08 +Create Date: 2025-02-14 09:58:18.227122 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "a113caac453e" +down_revision: Union[str, None] = "7980d239ea08" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Create identities table + op.create_table( + "identities", + sa.Column("id", sa.String(), nullable=False), + sa.Column("identifier_key", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("identity_type", sa.String(), nullable=False), + sa.Column("project_id", sa.String(), nullable=True), + # From OrganizationMixin + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + # Foreign key to organizations + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + # Composite unique constraint + sa.UniqueConstraint( + "identifier_key", + "project_id", + "organization_id", + name="unique_identifier_pid_org_id", + ), + sa.PrimaryKeyConstraint("id"), + ) + + # Add identity_id column to agents table + op.add_column("agents", sa.Column("identity_id", sa.String(), nullable=True)) + + # Add foreign key constraint + op.create_foreign_key("fk_agents_identity_id", "agents", "identities", ["identity_id"], ["id"], ondelete="CASCADE") + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # First remove the foreign key constraint and column from agents + op.drop_constraint("fk_agents_identity_id", "agents", type_="foreignkey") + op.drop_column("agents", "identity_id") + + # Then drop the table + op.drop_table("identities") diff --git a/alembic/versions/a1b2c3d4e5f6_add_index_to_step_metrics_run_id.py b/alembic/versions/a1b2c3d4e5f6_add_index_to_step_metrics_run_id.py new file mode 100644 index 0000000..6970a70 --- /dev/null +++ b/alembic/versions/a1b2c3d4e5f6_add_index_to_step_metrics_run_id.py @@ -0,0 +1,36 @@ +"""add index to step_metrics run_id + +Revision ID: a1b2c3d4e5f6 +Revises: d798609d65ff +Create Date: 2025-11-11 19:16:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "a1b2c3d4e5f6" +down_revision: Union[str, None] = "d798609d65ff" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Add index to step_metrics.run_id for efficient foreign key cascade operations + # This prevents full table scans when runs are deleted (ondelete="SET NULL") + op.create_index("ix_step_metrics_run_id", "step_metrics", ["run_id"], unique=False, if_not_exists=True) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.drop_index("ix_step_metrics_run_id", table_name="step_metrics", if_exists=True) diff --git a/alembic/versions/a1b2c3d4e5f7_add_blocks_conversations_table.py b/alembic/versions/a1b2c3d4e5f7_add_blocks_conversations_table.py new file mode 100644 index 0000000..57d2a2c --- /dev/null +++ b/alembic/versions/a1b2c3d4e5f7_add_blocks_conversations_table.py @@ -0,0 +1,48 @@ +"""Add blocks_conversations table for conversation-isolated blocks + +Revision ID: a1b2c3d4e5f7 +Revises: cf3c4d025dbc +Create Date: 2026-01-14 02:22:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "a1b2c3d4e5f7" +down_revision: Union[str, None] = "cf3c4d025dbc" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create blocks_conversations junction table + op.create_table( + "blocks_conversations", + sa.Column("conversation_id", sa.String(), nullable=False), + sa.Column("block_id", sa.String(), nullable=False), + sa.Column("block_label", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["conversation_id"], + ["conversations.id"], + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["block_id"], + ["block.id"], + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("conversation_id", "block_id", "block_label"), + sa.UniqueConstraint("conversation_id", "block_label", name="unique_label_per_conversation"), + sa.UniqueConstraint("conversation_id", "block_id", name="unique_conversation_block"), + ) + op.create_index("ix_blocks_conversations_block_id", "blocks_conversations", ["block_id"], unique=False) + + +def downgrade() -> None: + op.drop_index("ix_blocks_conversations_block_id", table_name="blocks_conversations") + op.drop_table("blocks_conversations") diff --git a/alembic/versions/a1b2c3d4e5f8_create_provider_trace_metadata_table.py b/alembic/versions/a1b2c3d4e5f8_create_provider_trace_metadata_table.py new file mode 100644 index 0000000..c4d3f1c --- /dev/null +++ b/alembic/versions/a1b2c3d4e5f8_create_provider_trace_metadata_table.py @@ -0,0 +1,59 @@ +"""create provider_trace_metadata table + +Revision ID: a1b2c3d4e5f8 +Revises: 9275f62ad282 +Create Date: 2026-01-28 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +revision: str = "a1b2c3d4e5f8" +down_revision: Union[str, None] = "9275f62ad282" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + if not settings.letta_pg_uri_no_default: + return + + op.create_table( + "provider_trace_metadata", + sa.Column("id", sa.String(), nullable=False), + sa.Column("step_id", sa.String(), nullable=True), + sa.Column("agent_id", sa.String(), nullable=True), + sa.Column("agent_tags", sa.JSON(), nullable=True), + sa.Column("call_type", sa.String(), nullable=True), + sa.Column("run_id", sa.String(), nullable=True), + sa.Column("source", sa.String(), nullable=True), + sa.Column("org_id", sa.String(), nullable=True), + sa.Column("user_id", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("created_at", "id"), + ) + op.create_index("ix_provider_trace_metadata_step_id", "provider_trace_metadata", ["step_id"], unique=False) + op.create_index("ix_provider_trace_metadata_id", "provider_trace_metadata", ["id"], unique=True) + + +def downgrade() -> None: + if not settings.letta_pg_uri_no_default: + return + + op.drop_index("ix_provider_trace_metadata_id", table_name="provider_trace_metadata") + op.drop_index("ix_provider_trace_metadata_step_id", table_name="provider_trace_metadata") + op.drop_table("provider_trace_metadata") diff --git a/alembic/versions/a3047a624130_add_identifier_key_to_agents.py b/alembic/versions/a3047a624130_add_identifier_key_to_agents.py new file mode 100644 index 0000000..320eefc --- /dev/null +++ b/alembic/versions/a3047a624130_add_identifier_key_to_agents.py @@ -0,0 +1,36 @@ +"""add identifier key to agents + +Revision ID: a3047a624130 +Revises: a113caac453e +Create Date: 2025-02-14 12:24:16.123456 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "a3047a624130" +down_revision: Union[str, None] = "a113caac453e" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.add_column("agents", sa.Column("identifier_key", sa.String(), nullable=True)) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.drop_column("agents", "identifier_key") diff --git a/alembic/versions/a3c7d62e08ca_add_callback_data_to_jobs_table.py b/alembic/versions/a3c7d62e08ca_add_callback_data_to_jobs_table.py new file mode 100644 index 0000000..cdc7985 --- /dev/null +++ b/alembic/versions/a3c7d62e08ca_add_callback_data_to_jobs_table.py @@ -0,0 +1,44 @@ +"""Add callback data to jobs table + +Revision ID: a3c7d62e08ca +Revises: 7b189006c97d +Create Date: 2025-04-17 17:40:16.224424 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "a3c7d62e08ca" +down_revision: Union[str, None] = "7b189006c97d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("jobs", sa.Column("callback_url", sa.String(), nullable=True)) + op.add_column("jobs", sa.Column("callback_sent_at", sa.DateTime(), nullable=True)) + op.add_column("jobs", sa.Column("callback_status_code", sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("jobs", "callback_status_code") + op.drop_column("jobs", "callback_sent_at") + op.drop_column("jobs", "callback_url") + # ### end Alembic commands ### diff --git a/alembic/versions/a66510f83fc2_add_ordered_agent_ids_to_groups.py b/alembic/versions/a66510f83fc2_add_ordered_agent_ids_to_groups.py new file mode 100644 index 0000000..6d41a37 --- /dev/null +++ b/alembic/versions/a66510f83fc2_add_ordered_agent_ids_to_groups.py @@ -0,0 +1,40 @@ +"""add ordered agent ids to groups + +Revision ID: a66510f83fc2 +Revises: bdddd421ec41 +Create Date: 2025-03-27 11:11:51.709498 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "a66510f83fc2" +down_revision: Union[str, None] = "bdddd421ec41" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("groups", sa.Column("agent_ids", sa.JSON(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("groups", "agent_ids") + # ### end Alembic commands ### diff --git a/alembic/versions/a91994b9752f_add_column_to_tools_table_to_contain_.py b/alembic/versions/a91994b9752f_add_column_to_tools_table_to_contain_.py new file mode 100644 index 0000000..3e2a4ad --- /dev/null +++ b/alembic/versions/a91994b9752f_add_column_to_tools_table_to_contain_.py @@ -0,0 +1,48 @@ +"""add column to tools table to contain function return limit return_char_limit + +Revision ID: a91994b9752f +Revises: e1a625072dbf +Create Date: 2024-12-09 18:27:25.650079 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.constants import FUNCTION_RETURN_CHAR_LIMIT +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "a91994b9752f" +down_revision: Union[str, None] = "e1a625072dbf" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("tools", sa.Column("return_char_limit", sa.Integer(), nullable=True)) + + # Populate `return_char_limit` column + op.execute( + f""" + UPDATE tools + SET return_char_limit = {FUNCTION_RETURN_CHAR_LIMIT} + """ + ) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("tools", "return_char_limit") + # ### end Alembic commands ### diff --git a/alembic/versions/af842aa6f743_add_tool_indexes_for_organization_id.py b/alembic/versions/af842aa6f743_add_tool_indexes_for_organization_id.py new file mode 100644 index 0000000..967532d --- /dev/null +++ b/alembic/versions/af842aa6f743_add_tool_indexes_for_organization_id.py @@ -0,0 +1,31 @@ +"""add tool indexes for organization_id + +Revision ID: af842aa6f743 +Revises: 175dd10fb916 +Create Date: 2025-12-07 15:30:43.407495 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "af842aa6f743" +down_revision: Union[str, None] = "175dd10fb916" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_index("ix_tools_organization_id", "tools", ["organization_id"], unique=False) + op.create_index("ix_tools_organization_id_name", "tools", ["organization_id", "name"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_index(op.f("ix_step_metrics_run_id"), "step_metrics", ["run_id"], unique=False) + op.create_index(op.f("idx_messages_step_id"), "messages", ["step_id"], unique=False) + # ### end Alembic commands ### diff --git a/alembic/versions/b183663c6769_add_trace_id_to_steps_table.py b/alembic/versions/b183663c6769_add_trace_id_to_steps_table.py new file mode 100644 index 0000000..25861a0 --- /dev/null +++ b/alembic/versions/b183663c6769_add_trace_id_to_steps_table.py @@ -0,0 +1,40 @@ +"""add trace id to steps table + +Revision ID: b183663c6769 +Revises: fdcdafdb11cf +Create Date: 2025-02-26 14:38:06.095556 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "b183663c6769" +down_revision: Union[str, None] = "fdcdafdb11cf" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("steps", sa.Column("trace_id", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("steps", "trace_id") + # ### end Alembic commands ### diff --git a/alembic/versions/b1c2d3e4f5a6_drop_unused_and_redundant_indexes.py b/alembic/versions/b1c2d3e4f5a6_drop_unused_and_redundant_indexes.py new file mode 100644 index 0000000..6909f61 --- /dev/null +++ b/alembic/versions/b1c2d3e4f5a6_drop_unused_and_redundant_indexes.py @@ -0,0 +1,71 @@ +"""drop unused and redundant indexes + +Revision ID: b1c2d3e4f5a6 +Revises: 2dbb2cf49e07 +Create Date: 2025-11-11 21:16:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "b1c2d3e4f5a6" +down_revision: Union[str, None] = "2dbb2cf49e07" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Drop unused indexes + op.drop_index("ix_passage_tags_archive_tag", table_name="passage_tags", if_exists=True) + op.drop_index("ix_jobs_created_at", table_name="jobs", if_exists=True) + op.drop_index("ix_block_project_id", table_name="block", if_exists=True) + op.drop_index("ix_block_label", table_name="block", if_exists=True) + + # Drop redundant indexes (covered by other composite indexes or FKs) + op.drop_index("ix_messages_run_id", table_name="messages", if_exists=True) # Redundant with ix_messages_run_sequence + op.drop_index("ix_files_agents_agent_id", table_name="files_agents", if_exists=True) # Redundant with FK index + op.drop_index( + "ix_agents_organization_id", table_name="agents", if_exists=True + ) # Redundant with ix_agents_organization_id_deployment_id + op.drop_index( + "ix_passage_tags_archive_id", table_name="passage_tags", if_exists=True + ) # Redundant with ix_passage_tags_archive_tag and ix_passage_tags_org_archive + op.drop_index( + "ix_blocks_block_label", table_name="blocks_agents", if_exists=True + ) # Redundant with ix_blocks_agents_block_label_agent_id + op.drop_index("ix_block_organization_id", table_name="block", if_exists=True) # Redundant with ix_block_org_project_template + op.drop_index( + "archival_passages_org_idx", table_name="archival_passages", if_exists=True + ) # Redundant with ix_archival_passages_org_archive + + # Drop unused table (leftover from PlanetScale migration) + op.drop_table("_planetscale_import", if_exists=True) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Re-create indexes in reverse order + op.create_index("archival_passages_org_idx", "archival_passages", ["organization_id"], unique=False, if_not_exists=True) + op.create_index("ix_block_organization_id", "block", ["organization_id"], unique=False, if_not_exists=True) + op.create_index("ix_blocks_block_label", "blocks_agents", ["block_label"], unique=False, if_not_exists=True) + op.create_index("ix_passage_tags_archive_id", "passage_tags", ["archive_id"], unique=False, if_not_exists=True) + op.create_index("ix_agents_organization_id", "agents", ["organization_id"], unique=False, if_not_exists=True) + op.create_index("ix_files_agents_agent_id", "files_agents", ["agent_id"], unique=False, if_not_exists=True) + op.create_index("ix_messages_run_id", "messages", ["run_id"], unique=False, if_not_exists=True) + op.create_index("ix_block_label", "block", ["label"], unique=False, if_not_exists=True) + op.create_index("ix_block_project_id", "block", ["project_id"], unique=False, if_not_exists=True) + op.create_index("ix_jobs_created_at", "jobs", ["created_at", "id"], unique=False, if_not_exists=True) + op.create_index("ix_passage_tags_archive_tag", "passage_tags", ["archive_id", "tag"], unique=False, if_not_exists=True) + + # Note: Not recreating _planetscale_import table in downgrade as it's application-specific diff --git a/alembic/versions/b2c3d4e5f6a8_add_llm_config_to_conversations.py b/alembic/versions/b2c3d4e5f6a8_add_llm_config_to_conversations.py new file mode 100644 index 0000000..b8e94dc --- /dev/null +++ b/alembic/versions/b2c3d4e5f6a8_add_llm_config_to_conversations.py @@ -0,0 +1,29 @@ +"""Add model and model_settings columns to conversations table for model overrides + +Revision ID: b2c3d4e5f6a8 +Revises: 3e54e2fa2f7e +Create Date: 2026-02-23 02:50:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "b2c3d4e5f6a8" +down_revision: Union[str, None] = "3e54e2fa2f7e" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("conversations", sa.Column("model", sa.String(), nullable=True)) + op.add_column("conversations", sa.Column("model_settings", sa.JSON(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("conversations", "model_settings") + op.drop_column("conversations", "model") diff --git a/alembic/versions/b6061da886ee_add_encrypted_columns.py b/alembic/versions/b6061da886ee_add_encrypted_columns.py new file mode 100644 index 0000000..a32c1d9 --- /dev/null +++ b/alembic/versions/b6061da886ee_add_encrypted_columns.py @@ -0,0 +1,39 @@ +"""add encrypted columns + +Revision ID: b6061da886ee +Revises: 89b595051e48 +Create Date: 2025-10-06 14:55:32.554544 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "b6061da886ee" +down_revision: Union[str, None] = "89b595051e48" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("agent_environment_variables", sa.Column("value_enc", sa.Text(), nullable=True)) + op.add_column("mcp_oauth", sa.Column("authorization_code_enc", sa.Text(), nullable=True)) + op.add_column("providers", sa.Column("api_key_enc", sa.Text(), nullable=True)) + op.add_column("providers", sa.Column("access_key_enc", sa.Text(), nullable=True)) + op.add_column("sandbox_environment_variables", sa.Column("value_enc", sa.Text(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("sandbox_environment_variables", "value_enc") + op.drop_column("providers", "access_key_enc") + op.drop_column("providers", "api_key_enc") + op.drop_column("mcp_oauth", "authorization_code_enc") + op.drop_column("agent_environment_variables", "value_enc") + # ### end Alembic commands ### diff --git a/alembic/versions/b6d7ca024aa9_add_agents_tags_table.py b/alembic/versions/b6d7ca024aa9_add_agents_tags_table.py new file mode 100644 index 0000000..c542994 --- /dev/null +++ b/alembic/versions/b6d7ca024aa9_add_agents_tags_table.py @@ -0,0 +1,61 @@ +"""Add agents tags table + +Revision ID: b6d7ca024aa9 +Revises: d14ae606614c +Create Date: 2024-11-06 10:48:08.424108 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "b6d7ca024aa9" +down_revision: Union[str, None] = "d14ae606614c" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "agents_tags", + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("tag", sa.String(), nullable=False), + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["agent_id"], + ["agents.id"], + ), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("agent_id", "id"), + sa.UniqueConstraint("agent_id", "tag", name="unique_agent_tag"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("agents_tags") + # ### end Alembic commands ### diff --git a/alembic/versions/b888f21b151f_add_vector_db_provider_to_source.py b/alembic/versions/b888f21b151f_add_vector_db_provider_to_source.py new file mode 100644 index 0000000..ba91726 --- /dev/null +++ b/alembic/versions/b888f21b151f_add_vector_db_provider_to_source.py @@ -0,0 +1,70 @@ +"""Add vector db provider to source + +Revision ID: b888f21b151f +Revises: 750dd87faa12 +Create Date: 2025-09-08 14:49:58.846429 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "b888f21b151f" +down_revision: Union[str, None] = "750dd87faa12" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # determine backfill value based on current pinecone settings + try: + from pinecone import IndexEmbed, PineconeAsyncio # noqa: F401 + + pinecone_available = True + except ImportError: + pinecone_available = False + + use_pinecone = all( + [ + pinecone_available, + settings.enable_pinecone, + settings.pinecone_api_key, + settings.pinecone_agent_index, + settings.pinecone_source_index, + ] + ) + + if settings.letta_pg_uri_no_default: + # commit required before altering enum in postgresql + connection = op.get_bind() + connection.execute(sa.text("COMMIT")) + connection.execute(sa.text("ALTER TYPE vectordbprovider ADD VALUE IF NOT EXISTS 'PINECONE'")) + connection.execute(sa.text("COMMIT")) + + vectordbprovider = sa.Enum("NATIVE", "TPUF", "PINECONE", name="vectordbprovider", create_type=False) + + op.add_column("sources", sa.Column("vector_db_provider", vectordbprovider, nullable=True)) + + if use_pinecone: + op.execute("UPDATE sources SET vector_db_provider = 'PINECONE' WHERE vector_db_provider IS NULL") + else: + op.execute("UPDATE sources SET vector_db_provider = 'NATIVE' WHERE vector_db_provider IS NULL") + + op.alter_column("sources", "vector_db_provider", nullable=False) + else: + op.add_column("sources", sa.Column("vector_db_provider", sa.String(), nullable=True)) + + if use_pinecone: + op.execute("UPDATE sources SET vector_db_provider = 'PINECONE' WHERE vector_db_provider IS NULL") + else: + op.execute("UPDATE sources SET vector_db_provider = 'NATIVE' WHERE vector_db_provider IS NULL") + + +def downgrade() -> None: + op.drop_column("sources", "vector_db_provider") + # enum type remains as postgresql doesn't support removing values diff --git a/alembic/versions/bdddd421ec41_add_privileged_tools_to_organization.py b/alembic/versions/bdddd421ec41_add_privileged_tools_to_organization.py new file mode 100644 index 0000000..cfd4b7e --- /dev/null +++ b/alembic/versions/bdddd421ec41_add_privileged_tools_to_organization.py @@ -0,0 +1,48 @@ +"""add privileged_tools to Organization + +Revision ID: bdddd421ec41 +Revises: 1e553a664210 +Create Date: 2025-03-21 17:55:30.405519 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "bdddd421ec41" +down_revision: Union[str, None] = "1e553a664210" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Step 1: Add `privileged_tools` column with nullable=True + op.add_column("organizations", sa.Column("privileged_tools", sa.Boolean(), nullable=True)) + + # fill in column with `False` + op.execute( + """ + UPDATE organizations + SET privileged_tools = False + """ + ) + + # Step 2: Make `privileged_tools` non-nullable + op.alter_column("organizations", "privileged_tools", nullable=False) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.drop_column("organizations", "privileged_tools") diff --git a/alembic/versions/bff040379479_add_block_history_tables.py b/alembic/versions/bff040379479_add_block_history_tables.py new file mode 100644 index 0000000..f897974 --- /dev/null +++ b/alembic/versions/bff040379479_add_block_history_tables.py @@ -0,0 +1,74 @@ +"""Add block history tables + +Revision ID: bff040379479 +Revises: a66510f83fc2 +Create Date: 2025-03-31 14:49:30.449052 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "bff040379479" +down_revision: Union[str, None] = "a66510f83fc2" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "block_history", + sa.Column("description", sa.Text(), nullable=True), + sa.Column("label", sa.String(), nullable=False), + sa.Column("value", sa.Text(), nullable=False), + sa.Column("limit", sa.BigInteger(), nullable=False), + sa.Column("metadata_", sa.JSON(), nullable=True), + sa.Column("actor_type", sa.String(), nullable=True), + sa.Column("actor_id", sa.String(), nullable=True), + sa.Column("block_id", sa.String(), nullable=False), + sa.Column("sequence_number", sa.Integer(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint(["block_id"], ["block.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_block_history_block_id_sequence", "block_history", ["block_id", "sequence_number"], unique=True) + op.add_column("block", sa.Column("current_history_entry_id", sa.String(), nullable=True)) + op.add_column("block", sa.Column("version", sa.Integer(), server_default="1", nullable=False)) + op.create_index(op.f("ix_block_current_history_entry_id"), "block", ["current_history_entry_id"], unique=False) + op.create_foreign_key("fk_block_current_history_entry", "block", "block_history", ["current_history_entry_id"], ["id"], use_alter=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("fk_block_current_history_entry", "block", type_="foreignkey") + op.drop_index(op.f("ix_block_current_history_entry_id"), table_name="block") + op.drop_column("block", "version") + op.drop_column("block", "current_history_entry_id") + op.drop_index("ix_block_history_block_id_sequence", table_name="block_history") + op.drop_table("block_history") + # ### end Alembic commands ### diff --git a/alembic/versions/c0ef3ff26306_add_token_to_mcp_server.py b/alembic/versions/c0ef3ff26306_add_token_to_mcp_server.py new file mode 100644 index 0000000..f11b70b --- /dev/null +++ b/alembic/versions/c0ef3ff26306_add_token_to_mcp_server.py @@ -0,0 +1,40 @@ +"""add_token_to_mcp_server + +Revision ID: c0ef3ff26306 +Revises: 1c6b6a38b713 +Create Date: 2025-06-14 14:59:53.835883 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "c0ef3ff26306" +down_revision: Union[str, None] = "1c6b6a38b713" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("mcp_server", sa.Column("token", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("mcp_server", "token") + # ### end Alembic commands ### diff --git a/alembic/versions/c3b1da3d1157_add_sender_id_to_message.py b/alembic/versions/c3b1da3d1157_add_sender_id_to_message.py new file mode 100644 index 0000000..df9454d --- /dev/null +++ b/alembic/versions/c3b1da3d1157_add_sender_id_to_message.py @@ -0,0 +1,40 @@ +"""add sender id to message + +Revision ID: c3b1da3d1157 +Revises: 0ceb975e0063 +Create Date: 2025-04-14 08:53:14.548061 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "c3b1da3d1157" +down_revision: Union[str, None] = "0ceb975e0063" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("messages", sa.Column("sender_id", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("messages", "sender_id") + # ### end Alembic commands ### diff --git a/alembic/versions/c41c87205254_add_default_requires_approval_field_on_.py b/alembic/versions/c41c87205254_add_default_requires_approval_field_on_.py new file mode 100644 index 0000000..cb13822 --- /dev/null +++ b/alembic/versions/c41c87205254_add_default_requires_approval_field_on_.py @@ -0,0 +1,31 @@ +"""add default requires approval field on tools + +Revision ID: c41c87205254 +Revises: 068588268b02 +Create Date: 2025-08-28 13:17:51.636159 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "c41c87205254" +down_revision: Union[str, None] = "068588268b02" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("tools", sa.Column("default_requires_approval", sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("tools", "default_requires_approval") + # ### end Alembic commands ### diff --git a/alembic/versions/c4eb5a907b38_add_file_controls_to_agent_state.py b/alembic/versions/c4eb5a907b38_add_file_controls_to_agent_state.py new file mode 100644 index 0000000..b9fa842 --- /dev/null +++ b/alembic/versions/c4eb5a907b38_add_file_controls_to_agent_state.py @@ -0,0 +1,33 @@ +"""Add file controls to agent state + +Revision ID: c4eb5a907b38 +Revises: cce9a6174366 +Create Date: 2025-07-21 15:56:57.413000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "c4eb5a907b38" +down_revision: Union[str, None] = "cce9a6174366" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("agents", sa.Column("max_files_open", sa.Integer(), nullable=True)) + op.add_column("agents", sa.Column("per_file_view_window_char_limit", sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("agents", "per_file_view_window_char_limit") + op.drop_column("agents", "max_files_open") + # ### end Alembic commands ### diff --git a/alembic/versions/c56081a05371_add_buffer_length_min_max_for_voice_.py b/alembic/versions/c56081a05371_add_buffer_length_min_max_for_voice_.py new file mode 100644 index 0000000..09ba1a8 --- /dev/null +++ b/alembic/versions/c56081a05371_add_buffer_length_min_max_for_voice_.py @@ -0,0 +1,42 @@ +"""Add buffer length min max for voice sleeptime + +Revision ID: c56081a05371 +Revises: 28b8765bdd0a +Create Date: 2025-04-30 16:03:41.213750 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "c56081a05371" +down_revision: Union[str, None] = "28b8765bdd0a" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("groups", sa.Column("max_message_buffer_length", sa.Integer(), nullable=True)) + op.add_column("groups", sa.Column("min_message_buffer_length", sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("groups", "min_message_buffer_length") + op.drop_column("groups", "max_message_buffer_length") + # ### end Alembic commands ### diff --git a/alembic/versions/c5d964280dff_add_passages_orm_drop_legacy_passages_.py b/alembic/versions/c5d964280dff_add_passages_orm_drop_legacy_passages_.py new file mode 100644 index 0000000..fdd8fc2 --- /dev/null +++ b/alembic/versions/c5d964280dff_add_passages_orm_drop_legacy_passages_.py @@ -0,0 +1,83 @@ +"""Add Passages ORM, drop legacy passages, cascading deletes for file-passages and user-jobs + +Revision ID: c5d964280dff +Revises: a91994b9752f +Create Date: 2024-12-10 15:05:32.335519 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "c5d964280dff" +down_revision: Union[str, None] = "a91994b9752f" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("passages", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True)) + op.add_column("passages", sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False)) + op.add_column("passages", sa.Column("_created_by_id", sa.String(), nullable=True)) + op.add_column("passages", sa.Column("_last_updated_by_id", sa.String(), nullable=True)) + + # Data migration step: + op.add_column("passages", sa.Column("organization_id", sa.String(), nullable=True)) + # Populate `organization_id` based on `user_id` + # Use a raw SQL query to update the organization_id + op.execute( + """ + UPDATE passages + SET organization_id = users.organization_id + FROM users + WHERE passages.user_id = users.id + """ + ) + + # Set `organization_id` as non-nullable after population + op.alter_column("passages", "organization_id", nullable=False) + + op.alter_column("passages", "text", existing_type=sa.VARCHAR(), nullable=False) + op.alter_column("passages", "embedding_config", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=False) + op.alter_column("passages", "metadata_", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=False) + op.alter_column("passages", "created_at", existing_type=postgresql.TIMESTAMP(timezone=True), nullable=False) + op.drop_index("passage_idx_user", table_name="passages") + op.create_foreign_key(None, "passages", "organizations", ["organization_id"], ["id"]) + op.create_foreign_key(None, "passages", "agents", ["agent_id"], ["id"]) + op.create_foreign_key(None, "passages", "files", ["file_id"], ["id"], ondelete="CASCADE") + op.drop_column("passages", "user_id") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("passages", sa.Column("user_id", sa.VARCHAR(), autoincrement=False, nullable=False)) + op.drop_constraint(None, "passages", type_="foreignkey") + op.drop_constraint(None, "passages", type_="foreignkey") + op.drop_constraint(None, "passages", type_="foreignkey") + op.create_index("passage_idx_user", "passages", ["user_id", "agent_id", "file_id"], unique=False) + op.alter_column("passages", "created_at", existing_type=postgresql.TIMESTAMP(timezone=True), nullable=True) + op.alter_column("passages", "metadata_", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=True) + op.alter_column("passages", "embedding_config", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=True) + op.alter_column("passages", "text", existing_type=sa.VARCHAR(), nullable=True) + op.drop_column("passages", "organization_id") + op.drop_column("passages", "_last_updated_by_id") + op.drop_column("passages", "_created_by_id") + op.drop_column("passages", "is_deleted") + op.drop_column("passages", "updated_at") + # ### end Alembic commands ### diff --git a/alembic/versions/c6c43222e2de_add_mcp_tools_table.py b/alembic/versions/c6c43222e2de_add_mcp_tools_table.py new file mode 100644 index 0000000..280ef3d --- /dev/null +++ b/alembic/versions/c6c43222e2de_add_mcp_tools_table.py @@ -0,0 +1,47 @@ +"""Add mcp_tools table + +Revision ID: c6c43222e2de +Revises: 6756d04c3ddb +Create Date: 2025-10-20 17:25:54.334037 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "c6c43222e2de" +down_revision: Union[str, None] = "6756d04c3ddb" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "mcp_tools", + sa.Column("mcp_server_id", sa.String(), nullable=False), + sa.Column("tool_id", sa.String(), nullable=False), + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("mcp_tools") + # ### end Alembic commands ### diff --git a/alembic/versions/c734cfc0d595_add_runs_metrics_table.py b/alembic/versions/c734cfc0d595_add_runs_metrics_table.py new file mode 100644 index 0000000..6f0db48 --- /dev/null +++ b/alembic/versions/c734cfc0d595_add_runs_metrics_table.py @@ -0,0 +1,55 @@ +"""add runs_metrics table + +Revision ID: c734cfc0d595 +Revises: 038e68cdf0df +Create Date: 2025-10-08 14:35:23.302204 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "c734cfc0d595" +down_revision: Union[str, None] = "038e68cdf0df" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "run_metrics", + sa.Column("id", sa.String(), nullable=False), + sa.Column("run_start_ns", sa.BigInteger(), nullable=True), + sa.Column("run_ns", sa.BigInteger(), nullable=True), + sa.Column("num_steps", sa.Integer(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("project_id", sa.String(), nullable=True), + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("base_template_id", sa.String(), nullable=True), + sa.Column("template_id", sa.String(), nullable=True), + sa.Column("deployment_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["id"], ["runs.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("run_metrics") + # ### end Alembic commands ### diff --git a/alembic/versions/c7ac45f69849_add_timezone_to_agents_table.py b/alembic/versions/c7ac45f69849_add_timezone_to_agents_table.py new file mode 100644 index 0000000..04b4577 --- /dev/null +++ b/alembic/versions/c7ac45f69849_add_timezone_to_agents_table.py @@ -0,0 +1,40 @@ +"""Add timezone to agents table + +Revision ID: c7ac45f69849 +Revises: 61ee53ec45a5 +Create Date: 2025-06-23 17:48:51.177458 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "c7ac45f69849" +down_revision: Union[str, None] = "61ee53ec45a5" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("agents", sa.Column("timezone", sa.String(), nullable=True, default="UTC")) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("agents", "timezone") + # ### end Alembic commands ### diff --git a/alembic/versions/c85a3d07c028_move_files_to_orm.py b/alembic/versions/c85a3d07c028_move_files_to_orm.py new file mode 100644 index 0000000..c0255a8 --- /dev/null +++ b/alembic/versions/c85a3d07c028_move_files_to_orm.py @@ -0,0 +1,65 @@ +"""Move files to orm + +Revision ID: c85a3d07c028 +Revises: cda66b6cb0d6 +Create Date: 2024-11-12 13:58:57.221081 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "c85a3d07c028" +down_revision: Union[str, None] = "cda66b6cb0d6" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("files", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True)) + op.add_column("files", sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False)) + op.add_column("files", sa.Column("_created_by_id", sa.String(), nullable=True)) + op.add_column("files", sa.Column("_last_updated_by_id", sa.String(), nullable=True)) + op.add_column("files", sa.Column("organization_id", sa.String(), nullable=True)) + # Populate `organization_id` based on `user_id` + # Use a raw SQL query to update the organization_id + op.execute( + """ + UPDATE files + SET organization_id = users.organization_id + FROM users + WHERE files.user_id = users.id + """ + ) + op.alter_column("files", "organization_id", nullable=False) + op.create_foreign_key(None, "files", "organizations", ["organization_id"], ["id"]) + op.create_foreign_key(None, "files", "sources", ["source_id"], ["id"]) + op.drop_column("files", "user_id") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("files", sa.Column("user_id", sa.VARCHAR(), autoincrement=False, nullable=False)) + op.drop_constraint(None, "files", type_="foreignkey") + op.drop_constraint(None, "files", type_="foreignkey") + op.drop_column("files", "organization_id") + op.drop_column("files", "_last_updated_by_id") + op.drop_column("files", "_created_by_id") + op.drop_column("files", "is_deleted") + op.drop_column("files", "updated_at") + # ### end Alembic commands ### diff --git a/alembic/versions/c96263433aef_add_file_name_to_source_passages.py b/alembic/versions/c96263433aef_add_file_name_to_source_passages.py new file mode 100644 index 0000000..18bd526 --- /dev/null +++ b/alembic/versions/c96263433aef_add_file_name_to_source_passages.py @@ -0,0 +1,49 @@ +"""Add file name to source passages + +Revision ID: c96263433aef +Revises: 9792f94e961d +Create Date: 2025-06-06 12:06:57.328127 +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "c96263433aef" +down_revision: Union[str, None] = "9792f94e961d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Add the new column + op.add_column("source_passages", sa.Column("file_name", sa.String(), nullable=True)) + + # Backfill file_name using SQL UPDATE JOIN + op.execute( + """ + UPDATE source_passages + SET file_name = files.file_name + FROM files + WHERE source_passages.file_id = files.id + """ + ) + + # Enforce non-null constraint after backfill + op.alter_column("source_passages", "file_name", nullable=False) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.drop_column("source_passages", "file_name") diff --git a/alembic/versions/cc8dc340836d_add_support_for_request_and_response_.py b/alembic/versions/cc8dc340836d_add_support_for_request_and_response_.py new file mode 100644 index 0000000..36a79dc --- /dev/null +++ b/alembic/versions/cc8dc340836d_add_support_for_request_and_response_.py @@ -0,0 +1,59 @@ +"""add support for request and response jsons from llm providers + +Revision ID: cc8dc340836d +Revises: 220856bbf43b +Create Date: 2025-05-19 14:25:41.999676 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "cc8dc340836d" +down_revision: Union[str, None] = "220856bbf43b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "provider_traces", + sa.Column("id", sa.String(), nullable=False), + sa.Column("request_json", sa.JSON(), nullable=False), + sa.Column("response_json", sa.JSON(), nullable=False), + sa.Column("step_id", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_step_id", "provider_traces", ["step_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_step_id", table_name="provider_traces") + op.drop_table("provider_traces") + # ### end Alembic commands ### diff --git a/alembic/versions/cce9a6174366_add_stop_reasons_to_steps_and_message_.py b/alembic/versions/cce9a6174366_add_stop_reasons_to_steps_and_message_.py new file mode 100644 index 0000000..14ac1cd --- /dev/null +++ b/alembic/versions/cce9a6174366_add_stop_reasons_to_steps_and_message_.py @@ -0,0 +1,42 @@ +"""add stop reasons to steps and message error flag + +Revision ID: cce9a6174366 +Revises: 2c059cad97cc +Create Date: 2025-07-10 13:56:17.383612 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "cce9a6174366" +down_revision: Union[str, None] = "2c059cad97cc" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("messages", sa.Column("is_err", sa.Boolean(), nullable=True)) + + # manually added to handle non-table creation enums + stopreasontype = sa.Enum( + "end_turn", "error", "invalid_tool_call", "max_steps", "no_tool_call", "tool_rule", "cancelled", name="stopreasontype" + ) + stopreasontype.create(op.get_bind()) + op.add_column("steps", sa.Column("stop_reason", stopreasontype, nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("steps", "stop_reason") + op.drop_column("messages", "is_err") + + stopreasontype = sa.Enum(name="stopreasontype") + stopreasontype.drop(op.get_bind()) + # ### end Alembic commands ### diff --git a/alembic/versions/cda66b6cb0d6_move_sources_to_orm.py b/alembic/versions/cda66b6cb0d6_move_sources_to_orm.py new file mode 100644 index 0000000..7ada943 --- /dev/null +++ b/alembic/versions/cda66b6cb0d6_move_sources_to_orm.py @@ -0,0 +1,73 @@ +"""Move sources to orm + +Revision ID: cda66b6cb0d6 +Revises: b6d7ca024aa9 +Create Date: 2024-11-07 13:29:57.186107 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "cda66b6cb0d6" +down_revision: Union[str, None] = "b6d7ca024aa9" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("sources", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True)) + op.add_column("sources", sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False)) + op.add_column("sources", sa.Column("_created_by_id", sa.String(), nullable=True)) + op.add_column("sources", sa.Column("_last_updated_by_id", sa.String(), nullable=True)) + + # Data migration step: + op.add_column("sources", sa.Column("organization_id", sa.String(), nullable=True)) + # Populate `organization_id` based on `user_id` + # Use a raw SQL query to update the organization_id + op.execute( + """ + UPDATE sources + SET organization_id = users.organization_id + FROM users + WHERE sources.user_id = users.id + """ + ) + + # Set `organization_id` as non-nullable after population + op.alter_column("sources", "organization_id", nullable=False) + + op.alter_column("sources", "embedding_config", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=False) + op.drop_index("sources_idx_user", table_name="sources") + op.create_foreign_key(None, "sources", "organizations", ["organization_id"], ["id"]) + op.drop_column("sources", "user_id") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("sources", sa.Column("user_id", sa.VARCHAR(), autoincrement=False, nullable=False)) + op.drop_constraint(None, "sources", type_="foreignkey") + op.create_index("sources_idx_user", "sources", ["user_id"], unique=False) + op.alter_column("sources", "embedding_config", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=True) + op.drop_column("sources", "organization_id") + op.drop_column("sources", "_last_updated_by_id") + op.drop_column("sources", "_created_by_id") + op.drop_column("sources", "is_deleted") + op.drop_column("sources", "updated_at") + # ### end Alembic commands ### diff --git a/alembic/versions/cdb3db091113_remove_unique_name_restriction_on_agents.py b/alembic/versions/cdb3db091113_remove_unique_name_restriction_on_agents.py new file mode 100644 index 0000000..f8713b4 --- /dev/null +++ b/alembic/versions/cdb3db091113_remove_unique_name_restriction_on_agents.py @@ -0,0 +1,38 @@ +"""Remove unique name restriction on agents + +Revision ID: cdb3db091113 +Revises: e20573fe9b86 +Create Date: 2025-01-10 15:36:08.728539 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "cdb3db091113" +down_revision: Union[str, None] = "e20573fe9b86" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("unique_org_agent_name", "agents", type_="unique") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint("unique_org_agent_name", "agents", ["organization_id", "name"]) + # ### end Alembic commands ### diff --git a/alembic/versions/cdd4a1c11aee_add_file_name_to_fileagent_association_.py b/alembic/versions/cdd4a1c11aee_add_file_name_to_fileagent_association_.py new file mode 100644 index 0000000..f808cdc --- /dev/null +++ b/alembic/versions/cdd4a1c11aee_add_file_name_to_fileagent_association_.py @@ -0,0 +1,72 @@ +"""Add file_name to FileAgent association table and FileContent table + +Revision ID: cdd4a1c11aee +Revises: 614c4e53b66e +Create Date: 2025-06-03 15:35:59.623704 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "cdd4a1c11aee" +down_revision: Union[str, None] = "614c4e53b66e" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "file_contents", + sa.Column("file_id", sa.String(), nullable=False), + sa.Column("text", sa.Text(), nullable=False), + sa.Column("id", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint(["file_id"], ["files.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("file_id", "id"), + ) + # add the column, nullable for now + op.add_column("files_agents", sa.Column("file_name", sa.String(), nullable=True)) + + # back-fill using a single UPDATE … FROM join + op.execute( + """ + UPDATE files_agents fa + SET file_name = f.file_name + FROM files f + WHERE fa.file_id = f.id; + """ + ) + + # now make it NOT NULL + op.alter_column("files_agents", "file_name", nullable=False) + op.create_index("ix_files_agents_agent_file_name", "files_agents", ["agent_id", "file_name"], unique=False) + op.create_unique_constraint("uq_files_agents_agent_file_name", "files_agents", ["agent_id", "file_name"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("uq_files_agents_agent_file_name", "files_agents", type_="unique") + op.drop_index("ix_files_agents_agent_file_name", table_name="files_agents") + op.drop_column("files_agents", "file_name") + op.drop_table("file_contents") + # ### end Alembic commands ### diff --git a/alembic/versions/cf3c4d025dbc_add_blocks_tags_table.py b/alembic/versions/cf3c4d025dbc_add_blocks_tags_table.py new file mode 100644 index 0000000..f2be82a --- /dev/null +++ b/alembic/versions/cf3c4d025dbc_add_blocks_tags_table.py @@ -0,0 +1,58 @@ +"""Add blocks tags table + +Revision ID: cf3c4d025dbc +Revises: 27de0f58e076 +Create Date: 2026-01-08 23:36:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "cf3c4d025dbc" +down_revision: Union[str, None] = "27de0f58e076" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Create blocks_tags table with timestamps and org scoping for filtering + # Note: Matches agents_tags structure but follows SQLite baseline pattern (no separate id column) + op.create_table( + "blocks_tags", + sa.Column("block_id", sa.String(), nullable=False), + sa.Column("tag", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["block_id"], + ["block.id"], + ), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("block_id", "tag"), + sa.UniqueConstraint("block_id", "tag", name="unique_block_tag"), + ) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.drop_table("blocks_tags") diff --git a/alembic/versions/d007f4ca66bf_npm_requirements_in_tools.py b/alembic/versions/d007f4ca66bf_npm_requirements_in_tools.py new file mode 100644 index 0000000..0972b68 --- /dev/null +++ b/alembic/versions/d007f4ca66bf_npm_requirements_in_tools.py @@ -0,0 +1,31 @@ +"""npm requirements in tools + +Revision ID: d007f4ca66bf +Revises: 74e860718e0d +Create Date: 2025-08-04 13:40:32.707036 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "d007f4ca66bf" +down_revision: Union[str, None] = "74e860718e0d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("tools", sa.Column("npm_requirements", sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("tools", "npm_requirements") + # ### end Alembic commands ### diff --git a/alembic/versions/d05669b60ebe_migrate_agents_to_orm.py b/alembic/versions/d05669b60ebe_migrate_agents_to_orm.py new file mode 100644 index 0000000..5fb1352 --- /dev/null +++ b/alembic/versions/d05669b60ebe_migrate_agents_to_orm.py @@ -0,0 +1,184 @@ +"""Migrate agents to orm + +Revision ID: d05669b60ebe +Revises: c5d964280dff +Create Date: 2024-12-12 10:25:31.825635 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "d05669b60ebe" +down_revision: Union[str, None] = "c5d964280dff" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "sources_agents", + sa.Column("agent_id", sa.String(), nullable=False), + sa.Column("source_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["agent_id"], + ["agents.id"], + ), + sa.ForeignKeyConstraint( + ["source_id"], + ["sources.id"], + ), + sa.PrimaryKeyConstraint("agent_id", "source_id"), + ) + op.drop_index("agent_source_mapping_idx_user", table_name="agent_source_mapping") + op.drop_table("agent_source_mapping") + op.add_column("agents", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True)) + op.add_column("agents", sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False)) + op.add_column("agents", sa.Column("_created_by_id", sa.String(), nullable=True)) + op.add_column("agents", sa.Column("_last_updated_by_id", sa.String(), nullable=True)) + op.add_column("agents", sa.Column("organization_id", sa.String(), nullable=True)) + # Populate `organization_id` based on `user_id` + # Use a raw SQL query to update the organization_id + op.execute( + """ + UPDATE agents + SET organization_id = users.organization_id + FROM users + WHERE agents.user_id = users.id + """ + ) + op.alter_column("agents", "organization_id", nullable=False) + op.alter_column("agents", "name", existing_type=sa.VARCHAR(), nullable=True) + op.drop_index("agents_idx_user", table_name="agents") + op.create_unique_constraint("unique_org_agent_name", "agents", ["organization_id", "name"]) + op.create_foreign_key(None, "agents", "organizations", ["organization_id"], ["id"]) + op.drop_column("agents", "tool_names") + op.drop_column("agents", "user_id") + op.drop_constraint("agents_tags_organization_id_fkey", "agents_tags", type_="foreignkey") + op.drop_column("agents_tags", "_created_by_id") + op.drop_column("agents_tags", "_last_updated_by_id") + op.drop_column("agents_tags", "updated_at") + op.drop_column("agents_tags", "id") + op.drop_column("agents_tags", "is_deleted") + op.drop_column("agents_tags", "created_at") + op.drop_column("agents_tags", "organization_id") + op.create_unique_constraint("unique_agent_block", "blocks_agents", ["agent_id", "block_id"]) + op.drop_constraint("fk_block_id_label", "blocks_agents", type_="foreignkey") + op.create_foreign_key( + "fk_block_id_label", "blocks_agents", "block", ["block_id", "block_label"], ["id", "label"], initially="DEFERRED", deferrable=True + ) + op.drop_column("blocks_agents", "_created_by_id") + op.drop_column("blocks_agents", "_last_updated_by_id") + op.drop_column("blocks_agents", "updated_at") + op.drop_column("blocks_agents", "id") + op.drop_column("blocks_agents", "is_deleted") + op.drop_column("blocks_agents", "created_at") + op.drop_constraint("unique_tool_per_agent", "tools_agents", type_="unique") + op.create_unique_constraint("unique_agent_tool", "tools_agents", ["agent_id", "tool_id"]) + op.drop_constraint("fk_tool_id", "tools_agents", type_="foreignkey") + op.drop_constraint("tools_agents_agent_id_fkey", "tools_agents", type_="foreignkey") + op.create_foreign_key(None, "tools_agents", "tools", ["tool_id"], ["id"], ondelete="CASCADE") + op.create_foreign_key(None, "tools_agents", "agents", ["agent_id"], ["id"], ondelete="CASCADE") + op.drop_column("tools_agents", "_created_by_id") + op.drop_column("tools_agents", "tool_name") + op.drop_column("tools_agents", "_last_updated_by_id") + op.drop_column("tools_agents", "updated_at") + op.drop_column("tools_agents", "id") + op.drop_column("tools_agents", "is_deleted") + op.drop_column("tools_agents", "created_at") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "tools_agents", + sa.Column("created_at", postgresql.TIMESTAMP(timezone=True), server_default=sa.text("now()"), autoincrement=False, nullable=True), + ) + op.add_column( + "tools_agents", sa.Column("is_deleted", sa.BOOLEAN(), server_default=sa.text("false"), autoincrement=False, nullable=False) + ) + op.add_column("tools_agents", sa.Column("id", sa.VARCHAR(), autoincrement=False, nullable=False)) + op.add_column( + "tools_agents", + sa.Column("updated_at", postgresql.TIMESTAMP(timezone=True), server_default=sa.text("now()"), autoincrement=False, nullable=True), + ) + op.add_column("tools_agents", sa.Column("_last_updated_by_id", sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column("tools_agents", sa.Column("tool_name", sa.VARCHAR(), autoincrement=False, nullable=False)) + op.add_column("tools_agents", sa.Column("_created_by_id", sa.VARCHAR(), autoincrement=False, nullable=True)) + op.drop_constraint(None, "tools_agents", type_="foreignkey") + op.drop_constraint(None, "tools_agents", type_="foreignkey") + op.create_foreign_key("tools_agents_agent_id_fkey", "tools_agents", "agents", ["agent_id"], ["id"]) + op.create_foreign_key("fk_tool_id", "tools_agents", "tools", ["tool_id"], ["id"]) + op.drop_constraint("unique_agent_tool", "tools_agents", type_="unique") + op.create_unique_constraint("unique_tool_per_agent", "tools_agents", ["agent_id", "tool_name"]) + op.add_column( + "blocks_agents", + sa.Column("created_at", postgresql.TIMESTAMP(timezone=True), server_default=sa.text("now()"), autoincrement=False, nullable=True), + ) + op.add_column( + "blocks_agents", sa.Column("is_deleted", sa.BOOLEAN(), server_default=sa.text("false"), autoincrement=False, nullable=False) + ) + op.add_column("blocks_agents", sa.Column("id", sa.VARCHAR(), autoincrement=False, nullable=False)) + op.add_column( + "blocks_agents", + sa.Column("updated_at", postgresql.TIMESTAMP(timezone=True), server_default=sa.text("now()"), autoincrement=False, nullable=True), + ) + op.add_column("blocks_agents", sa.Column("_last_updated_by_id", sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column("blocks_agents", sa.Column("_created_by_id", sa.VARCHAR(), autoincrement=False, nullable=True)) + op.drop_constraint("fk_block_id_label", "blocks_agents", type_="foreignkey") + op.create_foreign_key("fk_block_id_label", "blocks_agents", "block", ["block_id", "block_label"], ["id", "label"]) + op.drop_constraint("unique_agent_block", "blocks_agents", type_="unique") + op.add_column("agents_tags", sa.Column("organization_id", sa.VARCHAR(), autoincrement=False, nullable=False)) + op.add_column( + "agents_tags", + sa.Column("created_at", postgresql.TIMESTAMP(timezone=True), server_default=sa.text("now()"), autoincrement=False, nullable=True), + ) + op.add_column( + "agents_tags", sa.Column("is_deleted", sa.BOOLEAN(), server_default=sa.text("false"), autoincrement=False, nullable=False) + ) + op.add_column("agents_tags", sa.Column("id", sa.VARCHAR(), autoincrement=False, nullable=False)) + op.add_column( + "agents_tags", + sa.Column("updated_at", postgresql.TIMESTAMP(timezone=True), server_default=sa.text("now()"), autoincrement=False, nullable=True), + ) + op.add_column("agents_tags", sa.Column("_last_updated_by_id", sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column("agents_tags", sa.Column("_created_by_id", sa.VARCHAR(), autoincrement=False, nullable=True)) + op.create_foreign_key("agents_tags_organization_id_fkey", "agents_tags", "organizations", ["organization_id"], ["id"]) + op.add_column("agents", sa.Column("user_id", sa.VARCHAR(), autoincrement=False, nullable=False)) + op.add_column("agents", sa.Column("tool_names", postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) + op.drop_constraint(None, "agents", type_="foreignkey") + op.drop_constraint("unique_org_agent_name", "agents", type_="unique") + op.create_index("agents_idx_user", "agents", ["user_id"], unique=False) + op.alter_column("agents", "name", existing_type=sa.VARCHAR(), nullable=False) + op.drop_column("agents", "organization_id") + op.drop_column("agents", "_last_updated_by_id") + op.drop_column("agents", "_created_by_id") + op.drop_column("agents", "is_deleted") + op.drop_column("agents", "updated_at") + op.create_table( + "agent_source_mapping", + sa.Column("id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("user_id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("agent_id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("source_id", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint("id", name="agent_source_mapping_pkey"), + ) + op.create_index("agent_source_mapping_idx_user", "agent_source_mapping", ["user_id", "agent_id", "source_id"], unique=False) + op.drop_table("sources_agents") + # ### end Alembic commands ### diff --git a/alembic/versions/d06594144ef3_add_and_migrate_encrypted_columns_for_.py b/alembic/versions/d06594144ef3_add_and_migrate_encrypted_columns_for_.py new file mode 100644 index 0000000..f2d5441 --- /dev/null +++ b/alembic/versions/d06594144ef3_add_and_migrate_encrypted_columns_for_.py @@ -0,0 +1,313 @@ +"""add and migrate encrypted columns for mcp + +Revision ID: d06594144ef3 +Revises: 5d27a719b24d +Create Date: 2025-09-15 22:02:47.403970 + +""" + +import json +import os + +# Add the app directory to path to import our crypto utils +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy import JSON, String, Text +from sqlalchemy.sql import column, table + +from alembic import op +from letta.helpers.crypto_utils import CryptoUtils + +# revision identifiers, used by Alembic. +revision: str = "d06594144ef3" +down_revision: Union[str, None] = "5d27a719b24d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # First, add the new encrypted columns + op.add_column("mcp_oauth", sa.Column("access_token_enc", sa.Text(), nullable=True)) + op.add_column("mcp_oauth", sa.Column("refresh_token_enc", sa.Text(), nullable=True)) + op.add_column("mcp_oauth", sa.Column("client_secret_enc", sa.Text(), nullable=True)) + op.add_column("mcp_server", sa.Column("token_enc", sa.Text(), nullable=True)) + op.add_column("mcp_server", sa.Column("custom_headers_enc", sa.Text(), nullable=True)) + + # Check if encryption key is available + encryption_key = os.environ.get("LETTA_ENCRYPTION_KEY") + if not encryption_key: + print("WARNING: LETTA_ENCRYPTION_KEY not set. Skipping data encryption migration.") + print("You can run a separate migration script later to encrypt existing data.") + return + + # Get database connection + connection = op.get_bind() + + # Batch processing configuration + BATCH_SIZE = 1000 # Process 1000 rows at a time + + # Migrate mcp_oauth data + print("Migrating mcp_oauth encrypted fields...") + mcp_oauth = table( + "mcp_oauth", + column("id", String), + column("access_token", Text), + column("access_token_enc", Text), + column("refresh_token", Text), + column("refresh_token_enc", Text), + column("client_secret", Text), + column("client_secret_enc", Text), + ) + + # Count total rows to process + total_count_result = connection.execute( + sa.select(sa.func.count()) + .select_from(mcp_oauth) + .where( + sa.and_( + sa.or_(mcp_oauth.c.access_token.isnot(None), mcp_oauth.c.refresh_token.isnot(None), mcp_oauth.c.client_secret.isnot(None)), + # Only count rows that need encryption + sa.or_( + sa.and_(mcp_oauth.c.access_token.isnot(None), mcp_oauth.c.access_token_enc.is_(None)), + sa.and_(mcp_oauth.c.refresh_token.isnot(None), mcp_oauth.c.refresh_token_enc.is_(None)), + sa.and_(mcp_oauth.c.client_secret.isnot(None), mcp_oauth.c.client_secret_enc.is_(None)), + ), + ) + ) + ).scalar() + + if total_count_result and total_count_result > 0: + print(f"Found {total_count_result} mcp_oauth records that need encryption") + + encrypted_count = 0 + skipped_count = 0 + offset = 0 + + # Process in batches + while True: + # Select batch of rows + oauth_rows = connection.execute( + sa.select( + mcp_oauth.c.id, + mcp_oauth.c.access_token, + mcp_oauth.c.access_token_enc, + mcp_oauth.c.refresh_token, + mcp_oauth.c.refresh_token_enc, + mcp_oauth.c.client_secret, + mcp_oauth.c.client_secret_enc, + ) + .where( + sa.and_( + sa.or_( + mcp_oauth.c.access_token.isnot(None), + mcp_oauth.c.refresh_token.isnot(None), + mcp_oauth.c.client_secret.isnot(None), + ), + # Only select rows that need encryption + sa.or_( + sa.and_(mcp_oauth.c.access_token.isnot(None), mcp_oauth.c.access_token_enc.is_(None)), + sa.and_(mcp_oauth.c.refresh_token.isnot(None), mcp_oauth.c.refresh_token_enc.is_(None)), + sa.and_(mcp_oauth.c.client_secret.isnot(None), mcp_oauth.c.client_secret_enc.is_(None)), + ), + ) + ) + .order_by(mcp_oauth.c.id) # Ensure consistent ordering + .limit(BATCH_SIZE) + .offset(offset) + ).fetchall() + + if not oauth_rows: + break # No more rows to process + + # Prepare batch updates + batch_updates = [] + + for row in oauth_rows: + updates = {"id": row.id} + has_updates = False + + # Encrypt access_token if present and not already encrypted + if row.access_token and not row.access_token_enc: + try: + updates["access_token_enc"] = CryptoUtils.encrypt(row.access_token, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt access_token for mcp_oauth id={row.id}: {e}") + elif row.access_token_enc: + skipped_count += 1 + + # Encrypt refresh_token if present and not already encrypted + if row.refresh_token and not row.refresh_token_enc: + try: + updates["refresh_token_enc"] = CryptoUtils.encrypt(row.refresh_token, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt refresh_token for mcp_oauth id={row.id}: {e}") + elif row.refresh_token_enc: + skipped_count += 1 + + # Encrypt client_secret if present and not already encrypted + if row.client_secret and not row.client_secret_enc: + try: + updates["client_secret_enc"] = CryptoUtils.encrypt(row.client_secret, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt client_secret for mcp_oauth id={row.id}: {e}") + elif row.client_secret_enc: + skipped_count += 1 + + if has_updates: + batch_updates.append(updates) + encrypted_count += 1 + + # Execute batch update if there are updates + if batch_updates: + # Use bulk update for better performance + for update_data in batch_updates: + row_id = update_data.pop("id") + if update_data: # Only update if there are fields to update + connection.execute(mcp_oauth.update().where(mcp_oauth.c.id == row_id).values(**update_data)) + + # Progress indicator for large datasets + if encrypted_count > 0 and encrypted_count % 10000 == 0: + print(f" Progress: Encrypted {encrypted_count} mcp_oauth records...") + + offset += BATCH_SIZE + + # For very large datasets, commit periodically to avoid long transactions + if encrypted_count > 0 and encrypted_count % 50000 == 0: + connection.commit() + + print(f"mcp_oauth: Encrypted {encrypted_count} records, skipped {skipped_count} already encrypted fields") + else: + print("mcp_oauth: No records need encryption") + + # Migrate mcp_server data + print("Migrating mcp_server encrypted fields...") + mcp_server = table( + "mcp_server", + column("id", String), + column("token", String), + column("token_enc", Text), + column("custom_headers", JSON), + column("custom_headers_enc", Text), + ) + + # Count total rows to process + total_count_result = connection.execute( + sa.select(sa.func.count()) + .select_from(mcp_server) + .where( + sa.and_( + sa.or_(mcp_server.c.token.isnot(None), mcp_server.c.custom_headers.isnot(None)), + # Only count rows that need encryption + sa.or_( + sa.and_(mcp_server.c.token.isnot(None), mcp_server.c.token_enc.is_(None)), + sa.and_(mcp_server.c.custom_headers.isnot(None), mcp_server.c.custom_headers_enc.is_(None)), + ), + ) + ) + ).scalar() + + if total_count_result and total_count_result > 0: + print(f"Found {total_count_result} mcp_server records that need encryption") + + encrypted_count = 0 + skipped_count = 0 + offset = 0 + + # Process in batches + while True: + # Select batch of rows + server_rows = connection.execute( + sa.select( + mcp_server.c.id, + mcp_server.c.token, + mcp_server.c.token_enc, + mcp_server.c.custom_headers, + mcp_server.c.custom_headers_enc, + ) + .where( + sa.and_( + sa.or_(mcp_server.c.token.isnot(None), mcp_server.c.custom_headers.isnot(None)), + # Only select rows that need encryption + sa.or_( + sa.and_(mcp_server.c.token.isnot(None), mcp_server.c.token_enc.is_(None)), + sa.and_(mcp_server.c.custom_headers.isnot(None), mcp_server.c.custom_headers_enc.is_(None)), + ), + ) + ) + .order_by(mcp_server.c.id) # Ensure consistent ordering + .limit(BATCH_SIZE) + .offset(offset) + ).fetchall() + + if not server_rows: + break # No more rows to process + + # Prepare batch updates + batch_updates = [] + + for row in server_rows: + updates = {"id": row.id} + has_updates = False + + # Encrypt token if present and not already encrypted + if row.token and not row.token_enc: + try: + updates["token_enc"] = CryptoUtils.encrypt(row.token, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt token for mcp_server id={row.id}: {e}") + elif row.token_enc: + skipped_count += 1 + + # Encrypt custom_headers if present (JSON field) and not already encrypted + if row.custom_headers and not row.custom_headers_enc: + try: + # Convert JSON to string for encryption + headers_json = json.dumps(row.custom_headers) + updates["custom_headers_enc"] = CryptoUtils.encrypt(headers_json, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt custom_headers for mcp_server id={row.id}: {e}") + elif row.custom_headers_enc: + skipped_count += 1 + + if has_updates: + batch_updates.append(updates) + encrypted_count += 1 + + # Execute batch update if there are updates + if batch_updates: + # Use bulk update for better performance + for update_data in batch_updates: + row_id = update_data.pop("id") + if update_data: # Only update if there are fields to update + connection.execute(mcp_server.update().where(mcp_server.c.id == row_id).values(**update_data)) + + # Progress indicator for large datasets + if encrypted_count > 0 and encrypted_count % 10000 == 0: + print(f" Progress: Encrypted {encrypted_count} mcp_server records...") + + offset += BATCH_SIZE + + # For very large datasets, commit periodically to avoid long transactions + if encrypted_count > 0 and encrypted_count % 50000 == 0: + connection.commit() + + print(f"mcp_server: Encrypted {encrypted_count} records, skipped {skipped_count} already encrypted fields") + else: + print("mcp_server: No records need encryption") + print("Migration complete. Plaintext columns are retained for rollback safety.") + # ### end Alembic commands ### + + +def downgrade() -> None: + op.drop_column("mcp_server", "custom_headers_enc") + op.drop_column("mcp_server", "token_enc") + op.drop_column("mcp_oauth", "client_secret_enc") + op.drop_column("mcp_oauth", "refresh_token_enc") + op.drop_column("mcp_oauth", "access_token_enc") + # ### end Alembic commands ### diff --git a/alembic/versions/d0880aae6cee_add_compaction_settings_to_agents_table.py b/alembic/versions/d0880aae6cee_add_compaction_settings_to_agents_table.py new file mode 100644 index 0000000..20846a1 --- /dev/null +++ b/alembic/versions/d0880aae6cee_add_compaction_settings_to_agents_table.py @@ -0,0 +1,28 @@ +"""add compaction_settings to agents table + +Revision ID: d0880aae6cee +Revises: 2e5e90d3cdf8 +Create Date: 2025-12-10 16:17:23.595775 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.orm.custom_columns import CompactionSettingsColumn + +# revision identifiers, used by Alembic. +revision: str = "d0880aae6cee" +down_revision: Union[str, None] = "2e5e90d3cdf8" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column("agents", sa.Column("compaction_settings", CompactionSettingsColumn(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("agents", "compaction_settings") diff --git a/alembic/versions/d14ae606614c_move_organizations_users_tools_to_orm.py b/alembic/versions/d14ae606614c_move_organizations_users_tools_to_orm.py new file mode 100644 index 0000000..95c5fbe --- /dev/null +++ b/alembic/versions/d14ae606614c_move_organizations_users_tools_to_orm.py @@ -0,0 +1,100 @@ +"""Move organizations users tools to orm + +Revision ID: d14ae606614c +Revises: 9a505cc7eca9 +Create Date: 2024-11-05 15:03:12.350096 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +import letta +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "d14ae606614c" +down_revision: Union[str, None] = "9a505cc7eca9" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Delete all tools + op.execute("DELETE FROM tools") + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("agents", sa.Column("tool_rules", letta.orm.agent.ToolRulesColumn(), nullable=True)) + op.alter_column("block", "name", new_column_name="template_name", nullable=True) + op.add_column("organizations", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True)) + op.add_column("organizations", sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False)) + op.add_column("organizations", sa.Column("_created_by_id", sa.String(), nullable=True)) + op.add_column("organizations", sa.Column("_last_updated_by_id", sa.String(), nullable=True)) + op.add_column("tools", sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True)) + op.add_column("tools", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True)) + op.add_column("tools", sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False)) + op.add_column("tools", sa.Column("_created_by_id", sa.String(), nullable=True)) + op.add_column("tools", sa.Column("_last_updated_by_id", sa.String(), nullable=True)) + op.add_column("tools", sa.Column("organization_id", sa.String(), nullable=False)) + op.alter_column("tools", "tags", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=False) + op.alter_column("tools", "source_type", existing_type=sa.VARCHAR(), nullable=False) + op.alter_column("tools", "json_schema", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=False) + op.create_unique_constraint("uix_name_organization", "tools", ["name", "organization_id"]) + op.create_foreign_key(None, "tools", "organizations", ["organization_id"], ["id"]) + op.drop_column("tools", "user_id") + op.add_column("users", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True)) + op.add_column("users", sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False)) + op.add_column("users", sa.Column("_created_by_id", sa.String(), nullable=True)) + op.add_column("users", sa.Column("_last_updated_by_id", sa.String(), nullable=True)) + op.add_column("users", sa.Column("organization_id", sa.String(), nullable=True)) + # loop through all rows in the user table and set the _organization_id column from organization_id + op.execute('UPDATE "users" SET organization_id = org_id') + # set the _organization_id column to not nullable + op.alter_column("users", "organization_id", existing_type=sa.String(), nullable=False) + op.create_foreign_key(None, "users", "organizations", ["organization_id"], ["id"]) + op.drop_column("users", "org_id") + op.drop_column("users", "policies_accepted") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("users", sa.Column("policies_accepted", sa.BOOLEAN(), autoincrement=False, nullable=False)) + op.add_column("users", sa.Column("org_id", sa.VARCHAR(), autoincrement=False, nullable=True)) + op.drop_constraint(None, "users", type_="foreignkey") + op.drop_column("users", "organization_id") + op.drop_column("users", "_last_updated_by_id") + op.drop_column("users", "_created_by_id") + op.drop_column("users", "is_deleted") + op.drop_column("users", "updated_at") + op.add_column("tools", sa.Column("user_id", sa.VARCHAR(), autoincrement=False, nullable=True)) + op.drop_constraint(None, "tools", type_="foreignkey") + op.drop_constraint("uix_name_organization", "tools", type_="unique") + op.alter_column("tools", "json_schema", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=True) + op.alter_column("tools", "source_type", existing_type=sa.VARCHAR(), nullable=True) + op.alter_column("tools", "tags", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=True) + op.drop_column("tools", "organization_id") + op.drop_column("tools", "_last_updated_by_id") + op.drop_column("tools", "_created_by_id") + op.drop_column("tools", "is_deleted") + op.drop_column("tools", "updated_at") + op.drop_column("tools", "created_at") + op.drop_column("organizations", "_last_updated_by_id") + op.drop_column("organizations", "_created_by_id") + op.drop_column("organizations", "is_deleted") + op.drop_column("organizations", "updated_at") + op.add_column("block", sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=True)) + op.drop_column("block", "template_name") + op.drop_column("agents", "tool_rules") + # ### end Alembic commands ### diff --git a/alembic/versions/d211df879a5f_add_agent_id_to_steps.py b/alembic/versions/d211df879a5f_add_agent_id_to_steps.py new file mode 100644 index 0000000..6d5e57a --- /dev/null +++ b/alembic/versions/d211df879a5f_add_agent_id_to_steps.py @@ -0,0 +1,40 @@ +"""add agent id to steps + +Revision ID: d211df879a5f +Revises: 2f4ede6ae33b +Create Date: 2025-03-06 21:42:22.289345 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "d211df879a5f" +down_revision: Union[str, None] = "2f4ede6ae33b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("steps", sa.Column("agent_id", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("steps", "agent_id") + # ### end Alembic commands ### diff --git a/alembic/versions/d5103ee17ed5_add_template_fields_to_blocks_agents_.py b/alembic/versions/d5103ee17ed5_add_template_fields_to_blocks_agents_.py new file mode 100644 index 0000000..3904d73 --- /dev/null +++ b/alembic/versions/d5103ee17ed5_add_template_fields_to_blocks_agents_.py @@ -0,0 +1,47 @@ +"""add template fields to blocks agents groups + +Revision ID: d5103ee17ed5 +Revises: ffb17eb241fc +Create Date: 2025-08-26 15:45:32.949892 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "d5103ee17ed5" +down_revision: Union[str, None] = "ffb17eb241fc" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("agents", sa.Column("entity_id", sa.String(), nullable=True)) + op.add_column("agents", sa.Column("deployment_id", sa.String(), nullable=True)) + op.add_column("block", sa.Column("entity_id", sa.String(), nullable=True)) + op.add_column("block", sa.Column("base_template_id", sa.String(), nullable=True)) + op.add_column("block", sa.Column("template_id", sa.String(), nullable=True)) + op.add_column("block", sa.Column("deployment_id", sa.String(), nullable=True)) + op.add_column("groups", sa.Column("base_template_id", sa.String(), nullable=True)) + op.add_column("groups", sa.Column("template_id", sa.String(), nullable=True)) + op.add_column("groups", sa.Column("deployment_id", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("groups", "deployment_id") + op.drop_column("groups", "template_id") + op.drop_column("groups", "base_template_id") + op.drop_column("block", "deployment_id") + op.drop_column("block", "template_id") + op.drop_column("block", "base_template_id") + op.drop_column("block", "entity_id") + op.drop_column("agents", "deployment_id") + op.drop_column("agents", "entity_id") + # ### end Alembic commands ### diff --git a/alembic/versions/d6632deac81d_add_composite_index_to_messages_table.py b/alembic/versions/d6632deac81d_add_composite_index_to_messages_table.py new file mode 100644 index 0000000..2deb10f --- /dev/null +++ b/alembic/versions/d6632deac81d_add_composite_index_to_messages_table.py @@ -0,0 +1,38 @@ +"""Add composite index to messages table + +Revision ID: d6632deac81d +Revises: 54dec07619c4 +Create Date: 2024-12-18 13:38:56.511701 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "d6632deac81d" +down_revision: Union[str, None] = "54dec07619c4" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_index("ix_messages_agent_created_at", "messages", ["agent_id", "created_at"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_messages_agent_created_at", table_name="messages") + # ### end Alembic commands ### diff --git a/alembic/versions/d798609d65ff_add_index_on_messages_step_id.py b/alembic/versions/d798609d65ff_add_index_on_messages_step_id.py new file mode 100644 index 0000000..091a963 --- /dev/null +++ b/alembic/versions/d798609d65ff_add_index_on_messages_step_id.py @@ -0,0 +1,34 @@ +"""add_index_on_messages_step_id + +Revision ID: d798609d65ff +Revises: 89fd4648866b +Create Date: 2025-11-07 15:43:59.446292 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "d798609d65ff" +down_revision: Union[str, None] = "89fd4648866b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.create_index("idx_messages_step_id", "messages", ["step_id"], if_not_exists=True) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + op.drop_index("idx_messages_step_id", table_name="messages", if_exists=True) diff --git a/alembic/versions/dd049fbec729_add_index_on_agent_id_for_agent_env_var.py b/alembic/versions/dd049fbec729_add_index_on_agent_id_for_agent_env_var.py new file mode 100644 index 0000000..fd4e567 --- /dev/null +++ b/alembic/versions/dd049fbec729_add_index_on_agent_id_for_agent_env_var.py @@ -0,0 +1,38 @@ +"""Add index on agent_id for agent env var + +Revision ID: dd049fbec729 +Revises: 9ecbdbaa409f +Create Date: 2025-05-23 17:41:48.235405 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "dd049fbec729" +down_revision: Union[str, None] = "9ecbdbaa409f" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_index("idx_agent_environment_variables_agent_id", "agent_environment_variables", ["agent_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("idx_agent_environment_variables_agent_id", table_name="agent_environment_variables") + # ### end Alembic commands ### diff --git a/alembic/versions/ddb69be34a72_add_vector_db_namespace_fields_to_.py b/alembic/versions/ddb69be34a72_add_vector_db_namespace_fields_to_.py new file mode 100644 index 0000000..f1eb0b4 --- /dev/null +++ b/alembic/versions/ddb69be34a72_add_vector_db_namespace_fields_to_.py @@ -0,0 +1,33 @@ +"""Add vector db namespace fields to archive and agent state + +Revision ID: ddb69be34a72 +Revises: f3bf00ef6118 +Create Date: 2025-09-02 12:59:54.837863 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "ddb69be34a72" +down_revision: Union[str, None] = "f3bf00ef6118" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("agents", sa.Column("_vector_db_namespace", sa.String(), nullable=True)) + op.add_column("archives", sa.Column("_vector_db_namespace", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("archives", "_vector_db_namespace") + op.drop_column("agents", "_vector_db_namespace") + # ### end Alembic commands ### diff --git a/alembic/versions/ddecfe4902bc_add_prompts.py b/alembic/versions/ddecfe4902bc_add_prompts.py new file mode 100644 index 0000000..6e33a8c --- /dev/null +++ b/alembic/versions/ddecfe4902bc_add_prompts.py @@ -0,0 +1,42 @@ +"""add prompts + +Revision ID: ddecfe4902bc +Revises: c4eb5a907b38 +Create Date: 2025-07-21 15:58:13.357459 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "ddecfe4902bc" +down_revision: Union[str, None] = "c4eb5a907b38" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "prompts", + sa.Column("id", sa.String(), nullable=False), + sa.Column("prompt", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("project_id", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("prompts") + # ### end Alembic commands ### diff --git a/alembic/versions/dfafcf8210ca_add_model_endpoint_to_steps_table.py b/alembic/versions/dfafcf8210ca_add_model_endpoint_to_steps_table.py new file mode 100644 index 0000000..a450293 --- /dev/null +++ b/alembic/versions/dfafcf8210ca_add_model_endpoint_to_steps_table.py @@ -0,0 +1,40 @@ +"""add model endpoint to steps table + +Revision ID: dfafcf8210ca +Revises: f922ca16e42c +Create Date: 2025-02-04 16:45:34.132083 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "dfafcf8210ca" +down_revision: Union[str, None] = "f922ca16e42c" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("steps", sa.Column("model_endpoint", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("steps", "model_endpoint") + # ### end Alembic commands ### diff --git a/alembic/versions/e1a625072dbf_tweak_created_at_field_for_messages.py b/alembic/versions/e1a625072dbf_tweak_created_at_field_for_messages.py new file mode 100644 index 0000000..8a8b656 --- /dev/null +++ b/alembic/versions/e1a625072dbf_tweak_created_at_field_for_messages.py @@ -0,0 +1,40 @@ +"""Tweak created_at field for messages + +Revision ID: e1a625072dbf +Revises: 95badb46fdf9 +Create Date: 2024-12-07 14:28:27.643583 + +""" + +from typing import Sequence, Union + +from sqlalchemy.dialects import postgresql + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "e1a625072dbf" +down_revision: Union[str, None] = "95badb46fdf9" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("messages", "created_at", existing_type=postgresql.TIMESTAMP(timezone=True), nullable=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("messages", "created_at", existing_type=postgresql.TIMESTAMP(timezone=True), nullable=False) + # ### end Alembic commands ### diff --git a/alembic/versions/e20573fe9b86_add_tool_types.py b/alembic/versions/e20573fe9b86_add_tool_types.py new file mode 100644 index 0000000..afb2822 --- /dev/null +++ b/alembic/versions/e20573fe9b86_add_tool_types.py @@ -0,0 +1,79 @@ +"""Add tool types + +Revision ID: e20573fe9b86 +Revises: 915b68780108 +Create Date: 2025-01-09 15:11:47.779646 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op +from letta.constants import BASE_MEMORY_TOOLS, BASE_TOOLS +from letta.schemas.enums import ToolType +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "e20573fe9b86" +down_revision: Union[str, None] = "915b68780108" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Step 1: Add the column as nullable with no default + op.add_column("tools", sa.Column("tool_type", sa.String(), nullable=True)) + + # Step 2: Backpopulate the tool_type column based on tool name + # Define the list of Letta core tools + letta_core_value = ToolType.LETTA_CORE.value + letta_memory_core_value = ToolType.LETTA_MEMORY_CORE.value + custom_value = ToolType.CUSTOM.value + + # Update tool_type for Letta core tools + op.execute( + f""" + UPDATE tools + SET tool_type = '{letta_core_value}' + WHERE name IN ({",".join(f"'{name}'" for name in BASE_TOOLS)}); + """ + ) + + op.execute( + f""" + UPDATE tools + SET tool_type = '{letta_memory_core_value}' + WHERE name IN ({",".join(f"'{name}'" for name in BASE_MEMORY_TOOLS)}); + """ + ) + + # Update tool_type for all other tools + op.execute( + f""" + UPDATE tools + SET tool_type = '{custom_value}' + WHERE tool_type IS NULL; + """ + ) + + # Step 3: Alter the column to be non-nullable + op.alter_column("tools", "tool_type", nullable=False) + op.alter_column("tools", "json_schema", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=True) + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # Revert the changes made during the upgrade + op.alter_column("tools", "json_schema", existing_type=postgresql.JSON(astext_type=sa.Text()), nullable=False) + op.drop_column("tools", "tool_type") + # ### end Alembic commands ### diff --git a/alembic/versions/e67961ed7c32_add_enable_parallel_execution_to_tools_table.py b/alembic/versions/e67961ed7c32_add_enable_parallel_execution_to_tools_table.py new file mode 100644 index 0000000..7300157 --- /dev/null +++ b/alembic/versions/e67961ed7c32_add_enable_parallel_execution_to_tools_table.py @@ -0,0 +1,31 @@ +"""Add enable_parallel_execution to Tools table + +Revision ID: e67961ed7c32 +Revises: 8149a781ac1b +Create Date: 2025-10-17 15:47:00.447066 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "e67961ed7c32" +down_revision: Union[str, None] = "8149a781ac1b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("tools", sa.Column("enable_parallel_execution", sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("tools", "enable_parallel_execution") + # ### end Alembic commands ### diff --git a/alembic/versions/e78b4e82db30_add_cascading_deletes_for_sources_to_.py b/alembic/versions/e78b4e82db30_add_cascading_deletes_for_sources_to_.py new file mode 100644 index 0000000..3afa28b --- /dev/null +++ b/alembic/versions/e78b4e82db30_add_cascading_deletes_for_sources_to_.py @@ -0,0 +1,44 @@ +"""Add cascading deletes for sources to agents + +Revision ID: e78b4e82db30 +Revises: d6632deac81d +Create Date: 2024-12-20 16:30:17.095888 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "e78b4e82db30" +down_revision: Union[str, None] = "d6632deac81d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("sources_agents_agent_id_fkey", "sources_agents", type_="foreignkey") + op.drop_constraint("sources_agents_source_id_fkey", "sources_agents", type_="foreignkey") + op.create_foreign_key(None, "sources_agents", "sources", ["source_id"], ["id"], ondelete="CASCADE") + op.create_foreign_key(None, "sources_agents", "agents", ["agent_id"], ["id"], ondelete="CASCADE") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "sources_agents", type_="foreignkey") + op.drop_constraint(None, "sources_agents", type_="foreignkey") + op.create_foreign_key("sources_agents_source_id_fkey", "sources_agents", "sources", ["source_id"], ["id"]) + op.create_foreign_key("sources_agents_agent_id_fkey", "sources_agents", "agents", ["agent_id"], ["id"]) + # ### end Alembic commands ### diff --git a/alembic/versions/e991d2e3b428_add_monotonically_increasing_ids_to_.py b/alembic/versions/e991d2e3b428_add_monotonically_increasing_ids_to_.py new file mode 100644 index 0000000..6a63e0f --- /dev/null +++ b/alembic/versions/e991d2e3b428_add_monotonically_increasing_ids_to_.py @@ -0,0 +1,153 @@ +"""Add monotonically increasing IDs to messages table + +Revision ID: e991d2e3b428 +Revises: 74f2ede29317 +Create Date: 2025-04-01 17:02:59.820272 + +""" + +import sys +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "e991d2e3b428" +down_revision: Union[str, None] = "74f2ede29317" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +# --- Configuration --- +TABLE_NAME = "messages" +COLUMN_NAME = "sequence_id" +SEQUENCE_NAME = "message_seq_id" +INDEX_NAME = "ix_messages_agent_sequence" +UNIQUE_CONSTRAINT_NAME = f"uq_{TABLE_NAME}_{COLUMN_NAME}" + +# Columns to determine the order for back-filling existing data +ORDERING_COLUMNS = ["created_at", "id"] + + +def print_flush(message): + """Helper function to print and flush stdout immediately.""" + print(message) + sys.stdout.flush() + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + """Adds sequence_id, backfills data, adds constraints and index.""" + print_flush(f"\n--- Starting upgrade for revision {revision} ---") + + # Step 1: Add the sequence_id column to the table, initially allowing NULL values. + # This allows us to add and backfill data without immediately enforcing NOT NULL. + print_flush(f"Step 1: Adding nullable column '{COLUMN_NAME}' to table '{TABLE_NAME}'...") + op.add_column(TABLE_NAME, sa.Column(COLUMN_NAME, sa.BigInteger(), nullable=True)) + + # Step 2: Create a new PostgreSQL sequence. + # This sequence will later be used as the server-side default for generating new sequence_id values. + print_flush(f"Step 2: Creating sequence '{SEQUENCE_NAME}'...") + op.execute(f"CREATE SEQUENCE {SEQUENCE_NAME} START 1;") + + # Step 3: Backfill the sequence_id for existing rows based on a defined ordering. + # The SQL query does the following: + # - Uses a Common Table Expression named 'numbered_rows' to compute a row number for each row. + # - The ROW_NUMBER() window function assigns a sequential number (rn) to each row, ordered by the columns specified + # in ORDERING_COLUMNS (e.g., created_at, id) in ascending order. + # - The UPDATE statement then sets each row's sequence_id to its corresponding row number (rn) + # by joining the original table with the CTE on the id column. + print_flush(f"Step 3: Backfilling '{COLUMN_NAME}' based on order: {', '.join(ORDERING_COLUMNS)}...") + print_flush(" (This may take a while on large tables)") + try: + op.execute( + f""" + WITH numbered_rows AS ( + SELECT + id, + ROW_NUMBER() OVER (ORDER BY {", ".join(ORDERING_COLUMNS)} ASC) as rn + FROM {TABLE_NAME} + ) + UPDATE {TABLE_NAME} + SET {COLUMN_NAME} = numbered_rows.rn + FROM numbered_rows + WHERE {TABLE_NAME}.id = numbered_rows.id; + """ + ) + print_flush(" Backfill successful.") + except Exception as e: + print_flush(f"!!! ERROR during backfill: {e}") + print_flush("!!! Migration failed. Manual intervention might be needed.") + raise + + # Step 4: Set the sequence's next value to be one more than the current maximum sequence_id. + # The query works as follows: + # - It calculates the maximum value in the sequence_id column using MAX({COLUMN_NAME}). + # - COALESCE is used to default to 0 if there are no rows (i.e., the table is empty). + # - It then adds 1 to ensure that the next call to nextval() returns a number higher than any existing value. + # - The 'false' argument tells PostgreSQL that the next nextval() should return the value as-is, without pre-incrementing. + print_flush(f"Step 4: Setting sequence '{SEQUENCE_NAME}' to next value after backfill...") + op.execute( + f""" + SELECT setval('{SEQUENCE_NAME}', COALESCE(MAX({COLUMN_NAME}), 0) + 1, false) + FROM {TABLE_NAME}; + """ + ) + + # Step 5: Now that every row has a sequence_id, alter the column to be NOT NULL. + # This enforces that all rows must have a valid sequence_id. + print_flush(f"Step 5: Altering column '{COLUMN_NAME}' to NOT NULL...") + op.alter_column(TABLE_NAME, COLUMN_NAME, existing_type=sa.BigInteger(), nullable=False) + + # Step 6: Add a UNIQUE constraint on sequence_id to ensure its values remain distinct. + # This mirrors the model definition where sequence_id is defined as unique. + print_flush(f"Step 6: Creating unique constraint '{UNIQUE_CONSTRAINT_NAME}' on '{COLUMN_NAME}'...") + op.create_unique_constraint(UNIQUE_CONSTRAINT_NAME, TABLE_NAME, [COLUMN_NAME]) + + # Step 7: Set the server-side default for sequence_id so that future inserts automatically use the sequence. + # The server default calls nextval() on the sequence, and the "::regclass" cast helps PostgreSQL resolve the sequence name correctly. + print_flush(f"Step 7: Setting server default for '{COLUMN_NAME}' to use sequence '{SEQUENCE_NAME}'...") + op.alter_column(TABLE_NAME, COLUMN_NAME, existing_type=sa.BigInteger(), server_default=sa.text(f"nextval('{SEQUENCE_NAME}'::regclass)")) + + # Step 8: Create an index on (agent_id, sequence_id) to improve performance of queries filtering on these columns. + print_flush(f"Step 8: Creating index '{INDEX_NAME}' on (agent_id, {COLUMN_NAME})...") + op.create_index(INDEX_NAME, TABLE_NAME, ["agent_id", COLUMN_NAME], unique=False) + + print_flush(f"--- Upgrade for revision {revision} complete ---") + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + """Reverses the changes made in the upgrade function.""" + print_flush(f"\n--- Starting downgrade from revision {revision} ---") + + # 1. Drop the index + print_flush(f"Step 1: Dropping index '{INDEX_NAME}'...") + op.drop_index(INDEX_NAME, table_name=TABLE_NAME) + + # 2. Remove the server-side default + print_flush(f"Step 2: Removing server default from '{COLUMN_NAME}'...") + op.alter_column(TABLE_NAME, COLUMN_NAME, existing_type=sa.BigInteger(), server_default=None) + + # 3. Drop the unique constraint (using the explicit name) + print_flush(f"Step 3: Dropping unique constraint '{UNIQUE_CONSTRAINT_NAME}'...") + op.drop_constraint(UNIQUE_CONSTRAINT_NAME, TABLE_NAME, type_="unique") + + # 4. Drop the column (this implicitly removes the NOT NULL constraint) + print_flush(f"Step 4: Dropping column '{COLUMN_NAME}'...") + op.drop_column(TABLE_NAME, COLUMN_NAME) + + # 5. Drop the sequence + print_flush(f"Step 5: Dropping sequence '{SEQUENCE_NAME}'...") + op.execute(f"DROP SEQUENCE IF EXISTS {SEQUENCE_NAME};") # Use IF EXISTS for safety + + print_flush(f"--- Downgrade from revision {revision} complete ---") diff --git a/alembic/versions/ee2b43eea55e_add_request_id_to_steps_table.py b/alembic/versions/ee2b43eea55e_add_request_id_to_steps_table.py new file mode 100644 index 0000000..5bf9240 --- /dev/null +++ b/alembic/versions/ee2b43eea55e_add_request_id_to_steps_table.py @@ -0,0 +1,31 @@ +"""add request_id to steps table + +Revision ID: ee2b43eea55e +Revises: 39577145c45d +Create Date: 2025-12-17 13:48:08.642245 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "ee2b43eea55e" +down_revision: Union[str, None] = "39577145c45d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("steps", sa.Column("request_id", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("steps", "request_id") + # ### end Alembic commands ### diff --git a/alembic/versions/eff256d296cb_mcp_encrypted_data_migration.py b/alembic/versions/eff256d296cb_mcp_encrypted_data_migration.py new file mode 100644 index 0000000..d3de122 --- /dev/null +++ b/alembic/versions/eff256d296cb_mcp_encrypted_data_migration.py @@ -0,0 +1,300 @@ +"""mcp encrypted data migration + +Revision ID: eff256d296cb +Revises: 7f7933666957 +Create Date: 2025-09-16 16:01:58.943318 + +""" + +import json +import os + +# Add the app directory to path to import our crypto utils +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy import JSON, String, Text +from sqlalchemy.sql import column, table + +from alembic import op +from letta.helpers.crypto_utils import CryptoUtils + +# revision identifiers, used by Alembic. +revision: str = "eff256d296cb" +down_revision: Union[str, None] = "7f7933666957" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Check if encryption key is available + encryption_key = os.environ.get("LETTA_ENCRYPTION_KEY") + if not encryption_key: + print("WARNING: LETTA_ENCRYPTION_KEY not set. Skipping data encryption migration.") + print("You can run a separate migration script later to encrypt existing data.") + return + + # Get database connection + connection = op.get_bind() + + # Batch processing configuration + BATCH_SIZE = 1000 # Process 1000 rows at a time + + # Migrate mcp_oauth data + print("Migrating mcp_oauth encrypted fields...") + mcp_oauth = table( + "mcp_oauth", + column("id", String), + column("access_token", Text), + column("access_token_enc", Text), + column("refresh_token", Text), + column("refresh_token_enc", Text), + column("client_secret", Text), + column("client_secret_enc", Text), + ) + + # Count total rows to process + total_count_result = connection.execute( + sa.select(sa.func.count()) + .select_from(mcp_oauth) + .where( + sa.and_( + sa.or_(mcp_oauth.c.access_token.isnot(None), mcp_oauth.c.refresh_token.isnot(None), mcp_oauth.c.client_secret.isnot(None)), + # Only count rows that need encryption + sa.or_( + sa.and_(mcp_oauth.c.access_token.isnot(None), mcp_oauth.c.access_token_enc.is_(None)), + sa.and_(mcp_oauth.c.refresh_token.isnot(None), mcp_oauth.c.refresh_token_enc.is_(None)), + sa.and_(mcp_oauth.c.client_secret.isnot(None), mcp_oauth.c.client_secret_enc.is_(None)), + ), + ) + ) + ).scalar() + + if total_count_result and total_count_result > 0: + print(f"Found {total_count_result} mcp_oauth records that need encryption") + + encrypted_count = 0 + skipped_count = 0 + offset = 0 + + # Process in batches + while True: + # Select batch of rows + oauth_rows = connection.execute( + sa.select( + mcp_oauth.c.id, + mcp_oauth.c.access_token, + mcp_oauth.c.access_token_enc, + mcp_oauth.c.refresh_token, + mcp_oauth.c.refresh_token_enc, + mcp_oauth.c.client_secret, + mcp_oauth.c.client_secret_enc, + ) + .where( + sa.and_( + sa.or_( + mcp_oauth.c.access_token.isnot(None), + mcp_oauth.c.refresh_token.isnot(None), + mcp_oauth.c.client_secret.isnot(None), + ), + # Only select rows that need encryption + sa.or_( + sa.and_(mcp_oauth.c.access_token.isnot(None), mcp_oauth.c.access_token_enc.is_(None)), + sa.and_(mcp_oauth.c.refresh_token.isnot(None), mcp_oauth.c.refresh_token_enc.is_(None)), + sa.and_(mcp_oauth.c.client_secret.isnot(None), mcp_oauth.c.client_secret_enc.is_(None)), + ), + ) + ) + .order_by(mcp_oauth.c.id) # Ensure consistent ordering + .limit(BATCH_SIZE) + .offset(offset) + ).fetchall() + + if not oauth_rows: + break # No more rows to process + + # Prepare batch updates + batch_updates = [] + + for row in oauth_rows: + updates = {"id": row.id} + has_updates = False + + # Encrypt access_token if present and not already encrypted + if row.access_token and not row.access_token_enc: + try: + updates["access_token_enc"] = CryptoUtils.encrypt(row.access_token, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt access_token for mcp_oauth id={row.id}: {e}") + elif row.access_token_enc: + skipped_count += 1 + + # Encrypt refresh_token if present and not already encrypted + if row.refresh_token and not row.refresh_token_enc: + try: + updates["refresh_token_enc"] = CryptoUtils.encrypt(row.refresh_token, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt refresh_token for mcp_oauth id={row.id}: {e}") + elif row.refresh_token_enc: + skipped_count += 1 + + # Encrypt client_secret if present and not already encrypted + if row.client_secret and not row.client_secret_enc: + try: + updates["client_secret_enc"] = CryptoUtils.encrypt(row.client_secret, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt client_secret for mcp_oauth id={row.id}: {e}") + elif row.client_secret_enc: + skipped_count += 1 + + if has_updates: + batch_updates.append(updates) + encrypted_count += 1 + + # Execute batch update if there are updates + if batch_updates: + # Use bulk update for better performance + for update_data in batch_updates: + row_id = update_data.pop("id") + if update_data: # Only update if there are fields to update + connection.execute(mcp_oauth.update().where(mcp_oauth.c.id == row_id).values(**update_data)) + + # Progress indicator for large datasets + if encrypted_count > 0 and encrypted_count % 10000 == 0: + print(f" Progress: Encrypted {encrypted_count} mcp_oauth records...") + + offset += BATCH_SIZE + + # For very large datasets, commit periodically to avoid long transactions + if encrypted_count > 0 and encrypted_count % 50000 == 0: + connection.commit() + + print(f"mcp_oauth: Encrypted {encrypted_count} records, skipped {skipped_count} already encrypted fields") + else: + print("mcp_oauth: No records need encryption") + + # Migrate mcp_server data + print("Migrating mcp_server encrypted fields...") + mcp_server = table( + "mcp_server", + column("id", String), + column("token", String), + column("token_enc", Text), + column("custom_headers", JSON), + column("custom_headers_enc", Text), + ) + + # Count total rows to process + total_count_result = connection.execute( + sa.select(sa.func.count()) + .select_from(mcp_server) + .where( + sa.and_( + sa.or_(mcp_server.c.token.isnot(None), mcp_server.c.custom_headers.isnot(None)), + # Only count rows that need encryption + sa.or_( + sa.and_(mcp_server.c.token.isnot(None), mcp_server.c.token_enc.is_(None)), + sa.and_(mcp_server.c.custom_headers.isnot(None), mcp_server.c.custom_headers_enc.is_(None)), + ), + ) + ) + ).scalar() + + if total_count_result and total_count_result > 0: + print(f"Found {total_count_result} mcp_server records that need encryption") + + encrypted_count = 0 + skipped_count = 0 + offset = 0 + + # Process in batches + while True: + # Select batch of rows + server_rows = connection.execute( + sa.select( + mcp_server.c.id, + mcp_server.c.token, + mcp_server.c.token_enc, + mcp_server.c.custom_headers, + mcp_server.c.custom_headers_enc, + ) + .where( + sa.and_( + sa.or_(mcp_server.c.token.isnot(None), mcp_server.c.custom_headers.isnot(None)), + # Only select rows that need encryption + sa.or_( + sa.and_(mcp_server.c.token.isnot(None), mcp_server.c.token_enc.is_(None)), + sa.and_(mcp_server.c.custom_headers.isnot(None), mcp_server.c.custom_headers_enc.is_(None)), + ), + ) + ) + .order_by(mcp_server.c.id) # Ensure consistent ordering + .limit(BATCH_SIZE) + .offset(offset) + ).fetchall() + + if not server_rows: + break # No more rows to process + + # Prepare batch updates + batch_updates = [] + + for row in server_rows: + updates = {"id": row.id} + has_updates = False + + # Encrypt token if present and not already encrypted + if row.token and not row.token_enc: + try: + updates["token_enc"] = CryptoUtils.encrypt(row.token, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt token for mcp_server id={row.id}: {e}") + elif row.token_enc: + skipped_count += 1 + + # Encrypt custom_headers if present (JSON field) and not already encrypted + if row.custom_headers and not row.custom_headers_enc: + try: + # Convert JSON to string for encryption + headers_json = json.dumps(row.custom_headers) + updates["custom_headers_enc"] = CryptoUtils.encrypt(headers_json, encryption_key) + has_updates = True + except Exception as e: + print(f"Warning: Failed to encrypt custom_headers for mcp_server id={row.id}: {e}") + elif row.custom_headers_enc: + skipped_count += 1 + + if has_updates: + batch_updates.append(updates) + encrypted_count += 1 + + # Execute batch update if there are updates + if batch_updates: + # Use bulk update for better performance + for update_data in batch_updates: + row_id = update_data.pop("id") + if update_data: # Only update if there are fields to update + connection.execute(mcp_server.update().where(mcp_server.c.id == row_id).values(**update_data)) + + # Progress indicator for large datasets + if encrypted_count > 0 and encrypted_count % 10000 == 0: + print(f" Progress: Encrypted {encrypted_count} mcp_server records...") + + offset += BATCH_SIZE + + # For very large datasets, commit periodically to avoid long transactions + if encrypted_count > 0 and encrypted_count % 50000 == 0: + connection.commit() + + print(f"mcp_server: Encrypted {encrypted_count} records, skipped {skipped_count} already encrypted fields") + else: + print("mcp_server: No records need encryption") + print("Migration complete. Plaintext columns are retained for rollback safety.") + + +def downgrade() -> None: + pass diff --git a/alembic/versions/f2f78d62005c_add_letta_batch_job_id_to_llm_batch_job.py b/alembic/versions/f2f78d62005c_add_letta_batch_job_id_to_llm_batch_job.py new file mode 100644 index 0000000..5309d0f --- /dev/null +++ b/alembic/versions/f2f78d62005c_add_letta_batch_job_id_to_llm_batch_job.py @@ -0,0 +1,42 @@ +"""Add letta batch job id to llm_batch_job + +Revision ID: f2f78d62005c +Revises: c3b1da3d1157 +Create Date: 2025-04-17 15:58:43.705483 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "f2f78d62005c" +down_revision: Union[str, None] = "c3b1da3d1157" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("llm_batch_job", sa.Column("letta_batch_job_id", sa.String(), nullable=False)) + op.create_foreign_key(None, "llm_batch_job", "jobs", ["letta_batch_job_id"], ["id"], ondelete="CASCADE") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "llm_batch_job", type_="foreignkey") + op.drop_column("llm_batch_job", "letta_batch_job_id") + # ### end Alembic commands ### diff --git a/alembic/versions/f3bf00ef6118_add_approval_fields_to_message_model.py b/alembic/versions/f3bf00ef6118_add_approval_fields_to_message_model.py new file mode 100644 index 0000000..e7de5b3 --- /dev/null +++ b/alembic/versions/f3bf00ef6118_add_approval_fields_to_message_model.py @@ -0,0 +1,35 @@ +"""add approval fields to message model + +Revision ID: f3bf00ef6118 +Revises: 54c76f7cabca +Create Date: 2025-09-01 11:26:42.548009 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "f3bf00ef6118" +down_revision: Union[str, None] = "54c76f7cabca" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("messages", sa.Column("approval_request_id", sa.String(), nullable=True)) + op.add_column("messages", sa.Column("approve", sa.Boolean(), nullable=True)) + op.add_column("messages", sa.Column("denial_reason", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("messages", "denial_reason") + op.drop_column("messages", "approve") + op.drop_column("messages", "approval_request_id") + # ### end Alembic commands ### diff --git a/alembic/versions/f55542f37641_add_index_for_agent_tags_reversed_order.py b/alembic/versions/f55542f37641_add_index_for_agent_tags_reversed_order.py new file mode 100644 index 0000000..5a0d13d --- /dev/null +++ b/alembic/versions/f55542f37641_add_index_for_agent_tags_reversed_order.py @@ -0,0 +1,37 @@ +"""add index for agent_tags reversed order + +Revision ID: f55542f37641 +Revises: ddecfe4902bc +Create Date: 2025-07-24 18:00:30.773048 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "f55542f37641" +down_revision: Union[str, None] = "f5d26b0526e8" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Note some issues at least with pg8000 with concurrent index creation + # with op.get_context().autocommit_block(): + # op.create_index( + # op.f('ix_agent_tags_tag_agent_id'), + # "agents_tags", + # ['tag', 'agent_id'], + # unique=False, + # postgresql_concurrently=True, + # ) + op.create_index("ix_agents_tags_tag_agent_id", "agents_tags", ["tag", "agent_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_agents_tags_tag_agent_id", table_name="agents_tags") + # ### end Alembic commands ### diff --git a/alembic/versions/f595e0e8013e_adding_request_config_to_job_table.py b/alembic/versions/f595e0e8013e_adding_request_config_to_job_table.py new file mode 100644 index 0000000..ce79811 --- /dev/null +++ b/alembic/versions/f595e0e8013e_adding_request_config_to_job_table.py @@ -0,0 +1,40 @@ +"""adding request_config to Job table + +Revision ID: f595e0e8013e +Revises: 7f652fdd3dba +Create Date: 2025-01-14 14:34:34.203363 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "f595e0e8013e" +down_revision: Union[str, None] = "7f652fdd3dba" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("jobs", sa.Column("request_config", sa.JSON, nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("jobs", "request_config") + # ### end Alembic commands ### diff --git a/alembic/versions/f5d26b0526e8_add_mcp_oauth.py b/alembic/versions/f5d26b0526e8_add_mcp_oauth.py new file mode 100644 index 0000000..52c9f76 --- /dev/null +++ b/alembic/versions/f5d26b0526e8_add_mcp_oauth.py @@ -0,0 +1,67 @@ +"""add_mcp_oauth + +Revision ID: f5d26b0526e8 +Revises: ddecfe4902bc +Create Date: 2025-07-24 12:34:05.795355 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "f5d26b0526e8" +down_revision: Union[str, None] = "ddecfe4902bc" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "mcp_oauth", + sa.Column("id", sa.String(), nullable=False), + sa.Column("state", sa.String(length=255), nullable=False), + sa.Column("server_id", sa.String(length=255), nullable=True), + sa.Column("server_url", sa.Text(), nullable=False), + sa.Column("server_name", sa.Text(), nullable=False), + sa.Column("authorization_url", sa.Text(), nullable=True), + sa.Column("authorization_code", sa.Text(), nullable=True), + sa.Column("access_token", sa.Text(), nullable=True), + sa.Column("refresh_token", sa.Text(), nullable=True), + sa.Column("token_type", sa.String(length=50), nullable=False), + sa.Column("expires_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("scope", sa.Text(), nullable=True), + sa.Column("client_id", sa.Text(), nullable=True), + sa.Column("client_secret", sa.Text(), nullable=True), + sa.Column("redirect_uri", sa.Text(), nullable=True), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.ForeignKeyConstraint(["server_id"], ["mcp_server.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("state"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("mcp_oauth") + # ### end Alembic commands ### diff --git a/alembic/versions/f6cd5a1e519d_add_embedding_config_field_to_archives_.py b/alembic/versions/f6cd5a1e519d_add_embedding_config_field_to_archives_.py new file mode 100644 index 0000000..2cffe96 --- /dev/null +++ b/alembic/versions/f6cd5a1e519d_add_embedding_config_field_to_archives_.py @@ -0,0 +1,83 @@ +"""Add embedding config field to Archives table + +Revision ID: f6cd5a1e519d +Revises: c6c43222e2de +Create Date: 2025-10-23 16:33:53.661122 + +""" + +import json +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy import text + +import letta.orm +from alembic import op +from letta.schemas.embedding_config import EmbeddingConfig + +# revision identifiers, used by Alembic. +revision: str = "f6cd5a1e519d" +down_revision: Union[str, None] = "c6c43222e2de" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # step 1: add column as nullable + op.add_column("archives", sa.Column("embedding_config", letta.orm.custom_columns.EmbeddingConfigColumn(), nullable=True)) + + # step 2: backfill existing archives with embedding configs in batches + connection = op.get_bind() + + # default embedding config for archives without passages + default_config = EmbeddingConfig.default_config(model_name="letta") + default_embedding_config = default_config.model_dump() + + batch_size = 100 + processed = 0 + + # process in batches until no more archives need backfilling + while True: + archives = connection.execute( + text("SELECT id FROM archives WHERE embedding_config IS NULL LIMIT :batch_size"), {"batch_size": batch_size} + ).fetchall() + + if not archives: + break + + for archive in archives: + archive_id = archive[0] + + # check if archive has passages + first_passage = connection.execute( + text("SELECT embedding_config FROM archival_passages WHERE archive_id = :archive_id AND is_deleted = FALSE LIMIT 1"), + {"archive_id": archive_id}, + ).fetchone() + + if first_passage and first_passage[0]: + embedding_config = first_passage[0] + else: + embedding_config = default_embedding_config + + # serialize the embedding config to JSON string for raw SQL + config_json = json.dumps(embedding_config) + + connection.execute( + text("UPDATE archives SET embedding_config = :config WHERE id = :archive_id"), + {"config": config_json, "archive_id": archive_id}, + ) + + processed += len(archives) + print(f"Backfilled {processed} archives so far...") + + connection.execute(text("COMMIT")) + + print(f"Backfill complete. Total archives processed: {processed}") + + # step 3: make column non-nullable + op.alter_column("archives", "embedding_config", nullable=False) + + +def downgrade() -> None: + op.drop_column("archives", "embedding_config") diff --git a/alembic/versions/f7507eab4bb9_migrate_blocks_to_orm_model.py b/alembic/versions/f7507eab4bb9_migrate_blocks_to_orm_model.py new file mode 100644 index 0000000..7e1b030 --- /dev/null +++ b/alembic/versions/f7507eab4bb9_migrate_blocks_to_orm_model.py @@ -0,0 +1,83 @@ +"""Migrate blocks to orm model + +Revision ID: f7507eab4bb9 +Revises: c85a3d07c028 +Create Date: 2024-11-18 15:40:13.149438 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "f7507eab4bb9" +down_revision: Union[str, None] = "c85a3d07c028" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("block", sa.Column("is_template", sa.Boolean(), nullable=True)) + # Populate `is_template` column + op.execute( + """ + UPDATE block + SET is_template = COALESCE(template, FALSE) + """ + ) + + # Step 2: Make `is_template` non-nullable + op.alter_column("block", "is_template", nullable=False) + op.add_column("block", sa.Column("organization_id", sa.String(), nullable=True)) + # Populate `organization_id` based on `user_id` + # Use a raw SQL query to update the organization_id + op.execute( + """ + UPDATE block + SET organization_id = users.organization_id + FROM users + WHERE block.user_id = users.id + """ + ) + op.alter_column("block", "organization_id", nullable=False) + op.add_column("block", sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True)) + op.add_column("block", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True)) + op.add_column("block", sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False)) + op.add_column("block", sa.Column("_created_by_id", sa.String(), nullable=True)) + op.add_column("block", sa.Column("_last_updated_by_id", sa.String(), nullable=True)) + op.alter_column("block", "limit", existing_type=sa.BIGINT(), type_=sa.Integer(), nullable=False) + op.drop_index("block_idx_user", table_name="block") + op.create_foreign_key(None, "block", "organizations", ["organization_id"], ["id"]) + op.drop_column("block", "template") + op.drop_column("block", "user_id") + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("block", sa.Column("user_id", sa.VARCHAR(), autoincrement=False, nullable=True)) + op.add_column("block", sa.Column("template", sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.drop_constraint(None, "block", type_="foreignkey") + op.create_index("block_idx_user", "block", ["user_id"], unique=False) + op.alter_column("block", "limit", existing_type=sa.Integer(), type_=sa.BIGINT(), nullable=True) + op.drop_column("block", "_last_updated_by_id") + op.drop_column("block", "_created_by_id") + op.drop_column("block", "is_deleted") + op.drop_column("block", "updated_at") + op.drop_column("block", "created_at") + op.drop_column("block", "organization_id") + op.drop_column("block", "is_template") + # ### end Alembic commands ### diff --git a/alembic/versions/f7f757414d20_add_error_tracking_to_steps_table.py b/alembic/versions/f7f757414d20_add_error_tracking_to_steps_table.py new file mode 100644 index 0000000..41014c9 --- /dev/null +++ b/alembic/versions/f7f757414d20_add_error_tracking_to_steps_table.py @@ -0,0 +1,43 @@ +"""Add error tracking to steps table + +Revision ID: f7f757414d20 +Revises: 05c3bc564286 +Create Date: 2025-08-05 18:17:06.026153 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "f7f757414d20" +down_revision: Union[str, None] = "05c3bc564286" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Create the enum type first + stepstatus = sa.Enum("PENDING", "SUCCESS", "FAILED", "CANCELLED", name="stepstatus") + stepstatus.create(op.get_bind(), checkfirst=True) + + op.add_column("steps", sa.Column("error_type", sa.String(), nullable=True)) + op.add_column("steps", sa.Column("error_data", sa.JSON(), nullable=True)) + op.add_column("steps", sa.Column("status", stepstatus, nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("steps", "status") + op.drop_column("steps", "error_data") + op.drop_column("steps", "error_type") + + # Drop the enum type + stepstatus = sa.Enum("PENDING", "SUCCESS", "FAILED", "CANCELLED", name="stepstatus") + stepstatus.drop(op.get_bind(), checkfirst=True) + # ### end Alembic commands ### diff --git a/alembic/versions/f81ceea2c08d_create_sandbox_config_and_sandbox_env_.py b/alembic/versions/f81ceea2c08d_create_sandbox_config_and_sandbox_env_.py new file mode 100644 index 0000000..40d6467 --- /dev/null +++ b/alembic/versions/f81ceea2c08d_create_sandbox_config_and_sandbox_env_.py @@ -0,0 +1,82 @@ +"""Create sandbox config and sandbox env var tables + +Revision ID: f81ceea2c08d +Revises: c85a3d07c028 +Create Date: 2024-11-14 17:51:27.263561 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "f81ceea2c08d" +down_revision: Union[str, None] = "f7507eab4bb9" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "sandbox_configs", + sa.Column("id", sa.String(), nullable=False), + sa.Column("type", sa.Enum("E2B", "LOCAL", name="sandboxtype"), nullable=False), + sa.Column("config", sa.JSON(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("type", "organization_id", name="uix_type_organization"), + ) + op.create_table( + "sandbox_environment_variables", + sa.Column("id", sa.String(), nullable=False), + sa.Column("key", sa.String(), nullable=False), + sa.Column("value", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True), + sa.Column("is_deleted", sa.Boolean(), server_default=sa.text("FALSE"), nullable=False), + sa.Column("_created_by_id", sa.String(), nullable=True), + sa.Column("_last_updated_by_id", sa.String(), nullable=True), + sa.Column("organization_id", sa.String(), nullable=False), + sa.Column("sandbox_config_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], + ["organizations.id"], + ), + sa.ForeignKeyConstraint( + ["sandbox_config_id"], + ["sandbox_configs.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("key", "sandbox_config_id", name="uix_key_sandbox_config"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("sandbox_environment_variables") + op.drop_table("sandbox_configs") + # ### end Alembic commands ### diff --git a/alembic/versions/f895232c144a_backfill_composio_tools.py b/alembic/versions/f895232c144a_backfill_composio_tools.py new file mode 100644 index 0000000..bf3f951 --- /dev/null +++ b/alembic/versions/f895232c144a_backfill_composio_tools.py @@ -0,0 +1,60 @@ +"""Backfill composio tools + +Revision ID: f895232c144a +Revises: 25fc99e97839 +Create Date: 2025-01-16 14:21:33.764332 + +""" + +from typing import Sequence, Union + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "f895232c144a" +down_revision: Union[str, None] = "416b9d2db10b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + # Define the value for EXTERNAL_COMPOSIO (using string literal since enum was removed) + external_composio_value = "external_composio" + + # Update tool_type to EXTERNAL_COMPOSIO if the tags field includes "composio" + # This is super brittle and awful but no other way to do this + op.execute( + f""" + UPDATE tools + SET tool_type = '{external_composio_value}' + WHERE tags::jsonb @> '["composio"]'; + """ + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + # Use string literal for CUSTOM value + custom_value = "custom" + + # Update tool_type to CUSTOM if the tags field includes "composio" + # This is super brittle and awful but no other way to do this + op.execute( + f""" + UPDATE tools + SET tool_type = '{custom_value}' + WHERE tags::jsonb @> '["composio"]'; + """ + ) + # ### end Alembic commands ### diff --git a/alembic/versions/f922ca16e42c_add_project_and_template_id_to_agent.py b/alembic/versions/f922ca16e42c_add_project_and_template_id_to_agent.py new file mode 100644 index 0000000..169a6d7 --- /dev/null +++ b/alembic/versions/f922ca16e42c_add_project_and_template_id_to_agent.py @@ -0,0 +1,44 @@ +"""add project and template id to agent + +Revision ID: f922ca16e42c +Revises: 6fbe9cace832 +Create Date: 2025-01-29 16:57:48.161335 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "f922ca16e42c" +down_revision: Union[str, None] = "6fbe9cace832" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("agents", sa.Column("project_id", sa.String(), nullable=True)) + op.add_column("agents", sa.Column("template_id", sa.String(), nullable=True)) + op.add_column("agents", sa.Column("base_template_id", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("agents", "base_template_id") + op.drop_column("agents", "template_id") + op.drop_column("agents", "project_id") + # ### end Alembic commands ### diff --git a/alembic/versions/f9ad1c25fd2b_add_query_optimizing_runs_listing.py b/alembic/versions/f9ad1c25fd2b_add_query_optimizing_runs_listing.py new file mode 100644 index 0000000..61ba9ac --- /dev/null +++ b/alembic/versions/f9ad1c25fd2b_add_query_optimizing_runs_listing.py @@ -0,0 +1,29 @@ +"""add query optimizing runs listing + +Revision ID: f9ad1c25fd2b +Revises: 3bc3c031fbe4 +Create Date: 2025-10-04 00:44:06.663817 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "f9ad1c25fd2b" +down_revision: Union[str, None] = "3bc3c031fbe4" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_index("ix_messages_run_err_sequence", "messages", ["run_id", "is_err", "sequence_id"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("ix_messages_run_err_sequence", table_name="messages") + # ### end Alembic commands ### diff --git a/alembic/versions/fdcdafdb11cf_identity_properties_jsonb_to_json.py b/alembic/versions/fdcdafdb11cf_identity_properties_jsonb_to_json.py new file mode 100644 index 0000000..b553ecd --- /dev/null +++ b/alembic/versions/fdcdafdb11cf_identity_properties_jsonb_to_json.py @@ -0,0 +1,69 @@ +"""identity properties jsonb to json + +Revision ID: fdcdafdb11cf +Revises: 549eff097c71 +Create Date: 2025-02-21 10:30:49.937854 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from alembic import op +from letta.settings import settings + +# revision identifiers, used by Alembic. +revision: str = "fdcdafdb11cf" +down_revision: Union[str, None] = "549eff097c71" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "identities", + "properties", + existing_type=postgresql.JSONB(astext_type=sa.Text()), + type_=postgresql.JSON(astext_type=sa.Text()), + existing_nullable=False, + existing_server_default=sa.text("'[]'::jsonb"), + ) + op.drop_constraint("unique_identifier_without_project", "identities", type_="unique") + op.create_unique_constraint( + "unique_identifier_key_project_id_organization_id", + "identities", + ["identifier_key", "project_id", "organization_id"], + postgresql_nulls_not_distinct=True, + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # Skip this migration for SQLite + if not settings.letta_pg_uri_no_default: + return + + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("unique_identifier_key_project_id_organization_id", "identities", type_="unique") + op.create_unique_constraint( + "unique_identifier_without_project", + "identities", + ["identifier_key", "project_id", "organization_id"], + postgresql_nulls_not_distinct=True, + ) + op.alter_column( + "identities", + "properties", + existing_type=postgresql.JSON(astext_type=sa.Text()), + type_=postgresql.JSONB(astext_type=sa.Text()), + existing_nullable=False, + existing_server_default=sa.text("'[]'::jsonb"), + ) + # ### end Alembic commands ### diff --git a/alembic/versions/ffb17eb241fc_add_api_version_to_byok_providers.py b/alembic/versions/ffb17eb241fc_add_api_version_to_byok_providers.py new file mode 100644 index 0000000..28c2a28 --- /dev/null +++ b/alembic/versions/ffb17eb241fc_add_api_version_to_byok_providers.py @@ -0,0 +1,31 @@ +"""add api version to byok providers + +Revision ID: ffb17eb241fc +Revises: 5fb8bba2c373 +Create Date: 2025-08-12 14:35:26.375985 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "ffb17eb241fc" +down_revision: Union[str, None] = "5fb8bba2c373" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("providers", sa.Column("api_version", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("providers", "api_version") + # ### end Alembic commands ### diff --git a/assets/Letta-logo-RGB_GreyonOffBlack_cropped_small.png b/assets/Letta-logo-RGB_GreyonOffBlack_cropped_small.png new file mode 100644 index 0000000..73dab28 Binary files /dev/null and b/assets/Letta-logo-RGB_GreyonOffBlack_cropped_small.png differ diff --git a/assets/Letta-logo-RGB_GreyonTransparent_cropped_small.png b/assets/Letta-logo-RGB_GreyonTransparent_cropped_small.png new file mode 100644 index 0000000..7f461b1 Binary files /dev/null and b/assets/Letta-logo-RGB_GreyonTransparent_cropped_small.png differ diff --git a/assets/Letta-logo-RGB_OffBlackonTransparent_cropped_small.png b/assets/Letta-logo-RGB_OffBlackonTransparent_cropped_small.png new file mode 100644 index 0000000..39d5be4 Binary files /dev/null and b/assets/Letta-logo-RGB_OffBlackonTransparent_cropped_small.png differ diff --git a/assets/example_ade_screenshot.png b/assets/example_ade_screenshot.png new file mode 100644 index 0000000..fb75c43 Binary files /dev/null and b/assets/example_ade_screenshot.png differ diff --git a/assets/example_ade_screenshot_agents.png b/assets/example_ade_screenshot_agents.png new file mode 100644 index 0000000..e07df1f Binary files /dev/null and b/assets/example_ade_screenshot_agents.png differ diff --git a/assets/example_ade_screenshot_agents_light.png b/assets/example_ade_screenshot_agents_light.png new file mode 100644 index 0000000..d6e2392 Binary files /dev/null and b/assets/example_ade_screenshot_agents_light.png differ diff --git a/assets/example_ade_screenshot_light.png b/assets/example_ade_screenshot_light.png new file mode 100644 index 0000000..9e5c2af Binary files /dev/null and b/assets/example_ade_screenshot_light.png differ diff --git a/assets/letta_ade_screenshot.png b/assets/letta_ade_screenshot.png new file mode 100644 index 0000000..2717190 Binary files /dev/null and b/assets/letta_ade_screenshot.png differ diff --git a/certs/README.md b/certs/README.md new file mode 100644 index 0000000..87f56da --- /dev/null +++ b/certs/README.md @@ -0,0 +1,9 @@ +# About +These certs are used to set up a localhost https connection to the ADE. + +## Instructions +1. Install [mkcert](https://github.com/FiloSottile/mkcert) +2. Run `mkcert -install` +3. Run letta with the environment variable `LOCAL_HTTPS=true` +4. Access the app at [https://app.letta.com/development-servers/local/dashboard](https://app.letta.com/development-servers/local/dashboard) +5. Click "Add remote server" and enter `https://localhost:8283` as the URL, leave password blank unless you have secured your ADE with a password. diff --git a/certs/localhost-key.pem b/certs/localhost-key.pem new file mode 100644 index 0000000..363a191 --- /dev/null +++ b/certs/localhost-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDenaHTolfy9TzX +AUd60yPO1W0mpxdDTuxr2p3tBUaQJt5bEGzJbs1M0i5YVRK/SxtYZQvyqmI0ULKN +8+evKSEpJoDgLfFKM266jzKDSXd5XBQ3XuuxbKq6NV6qoTdweJ0zP0XXDUnKoTN6 +eMkUi8hD9P1TR3Ok3VGnT1wsdG0wPwRPDI/sD92GASL4ViUy/1Llrs7GjlOC+7M2 +GMoGifSHjmx2xgZ/K8cdD2q15iJJlhdbgCwfejcQlP7cmLtSJHH188EZeoFPEfNS +UpYNglS1kx0D/LC1ooTQRkCpLAnxeonMQZS5O5/q/zyxftkyKO+NInR6DtM0Uj8f +Gu5UDw1TAgMBAAECggEBANhqpkf4K0gm4V6j/7mISedp1RMenZ7xuyWfAqjJ2C+L +md8tuJSbAzsLmcKF8hPGEG9+zH685Xu2d99InpPKiFJY/DD0eP6JwbvcOl8nrN5u +hbjOrpNt8QvVlpKK6DqPB0Qq3tqSMIqs7D7D7bfrrGVkZmHvtJ0yC497t0AAb6XV +zTtnY9K6LVxb/t+eIDDX1AvE7i2WC+j1YgfexbM0VI/g4fveEVaKPFkWF3nSm0Ag +BmqzfGFUWKhBZmWpU0m86Zc45q575Bl64yXEQDYocUw3UfOp5/uF0lwuVe5Bpq/w +hIJgrW6RLzy20QFgPDxHhG3QdBpq4gB9BxCrMb+yhQECgYEA6jL1pD0drczxfaWC +bh+VsbVrRnz/0XDlPjaysO+qKsNP104gydeyyB6RcGnO8PssvFMCJNQlMhkEpp9x +bOwR36a4A2absC/osGgFH4pYyN2wqDb+qE2A3M+rnSGourd09y8BsCovN+5YsozK +HCnqjNWUweypU+RUvtM5jztsiOUCgYEA81ajdczpsysSn0xIFF0exvnnPLy/AiOR +uEFbPi0kavy7niwd609JFsSOwUXg2QavBNeoLH+YrQhueDoBJZANerLfLD8jqQxD +ojB6DkHwK5Vf9NIm8DZQ6trtf8xWGB/TuwpkWHm1wMdlCbmH38MukU4p6as7FKzT +8J57p/TfcdcCgYEAyDqfVzbFTBWfFbxOghZQ5mlj+RTfplHuPL2JEssk4oCvnzV1 +xPu8J2ozEDf2LIOiYLRbbd9OmcFX/5jr4aMHOP6R7p5oVz7uovub/bZLaBhZc8fo ++z2gAakvYR0o49H7l2XB/LpkOl51yNmj5mZT2Oq1zwKmVkotxiRS3smAZp0CgYAP +sOyFchs3xHVE9GRJe9+6MO8qSXl/p8+DtCMwFTUd+QIYJvwe6lPqNe6Go/zlwbqT +c1yS0f+EWODWu9bLF0jnOpWNgtzHz9Skpr+YH8Re6xju7oY4QyhgnJFoBkMe9x5u +FzN1SRPhRHpNcDtEwI9GK2YkfTgoEyTvhSiwIegurQKBgQDGkheCC7hqleNV3lGM +SfMUgyGt/1abZ82eAkdfeUrM37FeSbxuafyp0ICjZY0xsn6RUickHyXBJhkOGSJX +lGSvHwMsnXT30KAGd08ZqWmTSGmH6IrdVhrveY+e18ILXYgAkQ1T9tSKjeyFfK8m +dUWlFZHfdToFu1pn7yBgofMAmw== +-----END PRIVATE KEY----- diff --git a/certs/localhost.pem b/certs/localhost.pem new file mode 100644 index 0000000..8d4df20 --- /dev/null +++ b/certs/localhost.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEdjCCAt6gAwIBAgIQX/6Qs3c+lQq4+pcuUK7a7jANBgkqhkiG9w0BAQsFADCB +lTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTUwMwYDVQQLDCxzaHVi +QFNodWItTWVtR1BULURyaXZlci5sb2NhbCAoU2h1YmhhbSBOYWlrKTE8MDoGA1UE +AwwzbWtjZXJ0IHNodWJAU2h1Yi1NZW1HUFQtRHJpdmVyLmxvY2FsIChTaHViaGFt +IE5haWspMB4XDTI0MTIxMDE4MTgwMFoXDTI3MDMxMDE4MTgwMFowYDEnMCUGA1UE +ChMebWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTUwMwYDVQQLDCxzaHVi +QFNodWItTWVtR1BULURyaXZlci5sb2NhbCAoU2h1YmhhbSBOYWlrKTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAN6dodOiV/L1PNcBR3rTI87VbSanF0NO +7Gvane0FRpAm3lsQbMluzUzSLlhVEr9LG1hlC/KqYjRQso3z568pISkmgOAt8Uoz +brqPMoNJd3lcFDde67Fsqro1XqqhN3B4nTM/RdcNScqhM3p4yRSLyEP0/VNHc6Td +UadPXCx0bTA/BE8Mj+wP3YYBIvhWJTL/UuWuzsaOU4L7szYYygaJ9IeObHbGBn8r +xx0ParXmIkmWF1uALB96NxCU/tyYu1IkcfXzwRl6gU8R81JSlg2CVLWTHQP8sLWi +hNBGQKksCfF6icxBlLk7n+r/PLF+2TIo740idHoO0zRSPx8a7lQPDVMCAwEAAaN2 +MHQwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQY +MBaAFJ31vDww7/qA2mBtAN3GE+TZCqNeMCwGA1UdEQQlMCOCCWxvY2FsaG9zdIcE +fwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAYEAAy63DbPf +8iSWYmVgccFc5D+MpNgnWi6WsI5OTtRv66eV9+Vv9HseEVrSw8IVMoZt+peosi+K +0woVPT+bKCxlgkEClO7oZIUEMlzJq9sduISFV5fzFLMq8xhIIO5ud4zs1X/1GlrE +zAdq+YiZnbuKqLFSoPLZGrVclmiI3dLqp0LETZxVOiCGt52RRb87Mt9bQEHnP5LJ +EOJYZ1C7/qDDga3vFJ66Nisy015DpE7XXM5PASElpK9l4+yBOg9UdLSkd0VLm/Jm ++4rskdrSTiomU2TBd6Vys7nrn2K72ZOHOcbfFnPEet9z1L44xaddsaPE52ayu8PO +uxHl7rBr2Kzeuy22ppX09EpPdSnjrG6Sgojv4CCS6n8tAbhat8K0pTrzk1e7L8HT +Qy4P/LlViW56mfyM+02CurxbVOecCDdFPMwY357BXMnL6VmRrDtixh+XIXdyK2zS +aYhsbRFA7VJ1AM57gbPbDJElyIlvVetubilvfuOvvQX46cC/ZX5agzTd +-----END CERTIFICATE----- diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..756919c --- /dev/null +++ b/compose.yaml @@ -0,0 +1,65 @@ +services: + letta_db: + image: ankane/pgvector:v0.5.1 + networks: + default: + aliases: + - pgvector_db + - letta-db + environment: + - POSTGRES_USER=${LETTA_PG_USER:-letta} + - POSTGRES_PASSWORD=${LETTA_PG_PASSWORD:-letta} + - POSTGRES_DB=${LETTA_PG_DB:-letta} + volumes: + - ./.persist/pgdata:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - "${LETTA_PG_PORT:-5432}:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U letta"] + interval: 5s + timeout: 5s + retries: 5 + letta_server: + image: letta/letta:latest + hostname: letta-server + depends_on: + letta_db: + condition: service_healthy + ports: + - "8083:8083" + - "8283:8283" + env_file: + - .env + environment: + - LETTA_PG_URI=${LETTA_PG_URI:-postgresql://${LETTA_PG_USER:-letta}:${LETTA_PG_PASSWORD:-letta}@${LETTA_DB_HOST:-letta-db}:${LETTA_PG_PORT:-5432}/${LETTA_PG_DB:-letta}} + - LETTA_DEBUG=True + - OPENAI_API_KEY=${OPENAI_API_KEY} + - GROQ_API_KEY=${GROQ_API_KEY} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - OLLAMA_BASE_URL=${OLLAMA_BASE_URL} + - AZURE_API_KEY=${AZURE_API_KEY} + - AZURE_BASE_URL=${AZURE_BASE_URL} + - AZURE_API_VERSION=${AZURE_API_VERSION} + - GEMINI_API_KEY=${GEMINI_API_KEY} + - VLLM_API_BASE=${VLLM_API_BASE} + - OPENLLM_AUTH_TYPE=${OPENLLM_AUTH_TYPE} + - OPENLLM_API_KEY=${OPENLLM_API_KEY} + - LETTA_OTEL_EXPORTER_OTLP_ENDPOINT=${LETTA_OTEL_EXPORTER_OTLP_ENDPOINT} + - CLICKHOUSE_ENDPOINT=${CLICKHOUSE_ENDPOINT} + - CLICKHOUSE_DATABASE=${CLICKHOUSE_DATABASE} + - CLICKHOUSE_USERNAME=${CLICKHOUSE_USERNAME} + - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} + # volumes: + # - ./configs/server_config.yaml:/root/.letta/config # config file + # - ~/.letta/credentials:/root/.letta/credentials # credentials file + # Uncomment this line to mount a local directory for tool execution, and specify the mount path + # before running docker compose: `export LETTA_SANDBOX_MOUNT_PATH=$PWD/directory` + # - ${LETTA_SANDBOX_MOUNT_PATH:?}:/root/.letta/tool_execution_dir # mounted volume for tool execution + letta_nginx: + hostname: letta-nginx + image: nginx:stable-alpine3.17-slim + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + ports: + - "80:80" diff --git a/conf.yaml b/conf.yaml new file mode 100644 index 0000000..4493e63 --- /dev/null +++ b/conf.yaml @@ -0,0 +1,413 @@ +# Letta Configuration File +# Place at ~/.letta/conf.yaml, ./conf.yaml, or set LETTA_CONFIG_PATH +# Environment variables take precedence over config file values +# +# Top-level keys and their env var mappings: +# letta: -> LETTA_* +# model: -> Provider-prefixed (OPENAI_*, ANTHROPIC_*, etc.) +# tool: -> Prefix-based (E2B_*, MCP_*, TOOL_*, etc.) +# datadog: -> DD_* + +letta: + # ============================================================================= + # Core Settings (LETTA_*) + # ============================================================================= + debug: false + # environment: "" + + # Default handles + # default_llm_handle: "" + # default_embedding_handle: "" + + # SSE Streaming + enable_keepalive: true + keepalive_interval: 50.0 + enable_cancellation_aware_streaming: true + + # ============================================================================= + # PostgreSQL (LETTA_PG_*) + # ============================================================================= + pg: + # db: "" + # user: "" + # password: "" + # host: "" + # port: "" + # uri: "" + pool_size: 25 + max_overflow: 10 + pool_timeout: 30 + pool_recycle: 1800 + echo: false + + # Connection pool settings (LETTA_POOL_*) + pool: + pre_ping: true + use_lifo: true + + # Database settings (LETTA_DB_*) + # db: + # max_concurrent_sessions: "" + + disable_sqlalchemy_pooling: true + enable_db_pool_monitoring: true + db_pool_monitoring_interval: 30 + + # ============================================================================= + # Redis (LETTA_REDIS_*) + # ============================================================================= + redis: + # host: "" + port: 6379 + + # ============================================================================= + # Multi-Agent (LETTA_MULTI_AGENT_*) + # ============================================================================= + multi_agent: + send_message_max_retries: 3 + send_message_timeout: 1200 + concurrent_sends: 50 + + # ============================================================================= + # OTEL / Observability (LETTA_OTEL_*, LETTA_CLICKHOUSE_*) + # ============================================================================= + otel: + # exporter_otlp_endpoint: "" + preferred_temporality: 1 + + clickhouse: + # endpoint: "" + database: otel + username: default + # password: "" + + disable_tracing: false + llm_api_logging: true + track_last_agent_run: false + track_errored_messages: true + track_stop_reason: true + track_agent_run: true + track_provider_trace: true + + # ============================================================================= + # Uvicorn (LETTA_UVICORN_*) + # ============================================================================= + uvicorn: + workers: 1 + reload: false + timeout_keep_alive: 5 + + # Runtime settings + use_uvloop: false + use_granian: false + sqlalchemy_tracing: false + event_loop_threadpool_max_workers: 43 + + # ============================================================================= + # Experimental + # ============================================================================= + use_vertex_structured_outputs_experimental: false + use_asyncio_shield: true + + # ============================================================================= + # Lettuce (LETTA_USE_LETTUCE_*) + # ============================================================================= + use_lettuce_for_file_uploads: false + + # ============================================================================= + # Batch Job Polling (LETTA_POLL_*, LETTA_BATCH_*) + # ============================================================================= + enable_batch_job_polling: false + poll_running_llm_batches_interval_seconds: 300 + poll_lock_retry_interval_seconds: 480 + batch_job_polling_lookback_weeks: 2 + # batch_job_polling_batch_size: "" + + # ============================================================================= + # LLM Timeouts (LETTA_LLM_*) + # ============================================================================= + llm: + request_timeout_seconds: 60.0 + stream_timeout_seconds: 600.0 + + # ============================================================================= + # Pinecone (LETTA_PINECONE_*, LETTA_ENABLE_PINECONE, LETTA_UPSERT_PINECONE_INDICES) + # ============================================================================= + enable_pinecone: false + upsert_pinecone_indices: false + pinecone: + # api_key: "" + source_index: sources + agent_index: recall + + # ============================================================================= + # Turbopuffer (LETTA_TPUF_*, LETTA_USE_TPUF, LETTA_EMBED_*) + # ============================================================================= + use_tpuf: false + embed_all_messages: false + embed_tools: false + tpuf: + # api_key: "" + region: gcp-us-central1 + + # ============================================================================= + # File Processing (LETTA_FILE_PROCESSING_*) + # ============================================================================= + file_processing: + timeout_minutes: 30 + timeout_error_message: "File processing timed out after {} minutes. Please try again." + + # ============================================================================= + # Letta Client (LETTA_DEFAULT_*) + # ============================================================================= + default_base_url: http://localhost:8283 + # default_token: "" + + # ============================================================================= + # Agent Architecture + # ============================================================================= + use_letta_v1_agent: false + archival_memory_token_limit: 8192 + + # ============================================================================= + # Security + # ============================================================================= + no_default_actor: false + # encryption_key: "" + + # ============================================================================= + # OCR + # ============================================================================= + # mistral_api_key: "" + + # ============================================================================= + # Summarizer (LETTA_SUMMARIZER_*) + # ============================================================================= + summarizer: + mode: partial_evict_message_buffer_mode + message_buffer_limit: 60 + message_buffer_min: 15 + enable_summarization: true + max_summarization_retries: 3 + partial_evict_summarizer_percentage: 0.30 + evict_all_messages: false + max_summarizer_retries: 3 + memory_warning_threshold: 0.75 + send_memory_warning_message: false + desired_memory_token_pressure: 0.3 + keep_last_n_messages: 0 + + # ============================================================================= + # Logging (LETTA_LOGGING_*) + # ============================================================================= + logging: + debug: false + json_logging: false + log_level: WARNING + verbose_telemetry_logging: false + + # ============================================================================= + # Telemetry (LETTA_TELEMETRY_*) + # ============================================================================= + telemetry: + enable_datadog: false + provider_trace_backend: postgres + socket_path: /var/run/telemetry/telemetry.sock + provider_trace_pg_metadata_only: false + # source: "" + + # Datadog settings (LETTA_TELEMETRY_DATADOG_*) + datadog: + agent_host: localhost + agent_port: 8126 + service_name: letta-server + profiling_enabled: false + profiling_memory_enabled: false + profiling_heap_enabled: false + # git_repository_url: "" + # git_commit_sha: "" + main_package: letta + +# ============================================================================= +# Model Settings (-> OPENAI_*, ANTHROPIC_*, AWS_*, etc.) +# ============================================================================= +model: + # Global settings + global_max_context_window_limit: 128000 + inner_thoughts_kwarg: thinking + default_prompt_formatter: chatml + + # OpenAI (-> OPENAI_*) + openai: + # api_key: "" + api_base: https://api.openai.com/v1 + + # Anthropic (-> ANTHROPIC_*) + anthropic: + # api_key: "" + max_retries: 3 + sonnet_1m: false + + # Azure OpenAI (-> AZURE_*) + azure: + # api_key: "" + # base_url: "" + api_version: "2024-09-01-preview" + + # Google Gemini (-> GEMINI_*) + gemini: + # api_key: "" + base_url: https://generativelanguage.googleapis.com/ + force_minimum_thinking_budget: false + max_retries: 5 + timeout_seconds: 600.0 + + # Google Vertex (-> GOOGLE_CLOUD_*) + # google_cloud: + # project: "" + # location: "" + + # AWS Bedrock (-> AWS_*, BEDROCK_*) + aws: + # access_key_id: "" + # secret_access_key: "" + default_region: us-east-1 + + bedrock: + anthropic_version: bedrock-2023-05-31 + + # OpenRouter (-> OPENROUTER_*) + # openrouter: + # api_key: "" + # referer: "" + # title: "" + # handle_base: "" + + # Groq (-> GROQ_*) + # groq: + # api_key: "" + + # Together (-> TOGETHER_*) + # together: + # api_key: "" + + # DeepSeek (-> DEEPSEEK_*) + # deepseek: + # api_key: "" + + # xAI/Grok (-> XAI_*) + # xai: + # api_key: "" + + # Z.ai/ZhipuAI (-> ZAI_*) + zai: + # api_key: "" + base_url: https://api.z.ai/api/paas/v4/ + + # MiniMax (-> MINIMAX_*) + # minimax: + # api_key: "" + + # Ollama (-> OLLAMA_*) + # ollama: + # base_url: "" + + # vLLM (-> VLLM_*) + # vllm: + # api_base: "" + # handle_base: "" + + # SGLang (-> SGLANG_*) + # sglang: + # api_base: "" + # handle_base: "" + + # LM Studio (-> LMSTUDIO_*) + # lmstudio: + # base_url: "" + + # OpenLLM (-> OPENLLM_*) + # openllm: + # auth_type: "" + # api_key: "" + +# ============================================================================= +# Tool Settings (-> E2B_*, MCP_*, MODAL_*, TOOL_*, etc.) +# ============================================================================= +tool: + # E2B Sandbox (-> E2B_*) + # e2b: + # api_key: "" + # sandbox_template_id: "" + + # Modal Sandbox (-> MODAL_*) + # modal: + # token_id: "" + # token_secret: "" + + # Search Providers (-> TAVILY_*, EXA_*) + # tavily: + # api_key: "" + + # exa: + # api_key: "" + + # Local Sandbox (-> TOOL_*) + tool: + # exec_dir: "" + sandbox_timeout: 180 + # exec_venv_name: "" + exec_autoreload_venv: true + + # MCP (-> MCP_*) + mcp: + connect_to_server_timeout: 30.0 + list_tools_timeout: 30.0 + execute_tool_timeout: 60.0 + read_from_config: false + disable_stdio: true + +# ============================================================================= +# Datadog Agent Settings (-> DD_*) +# ============================================================================= +# datadog: +# site: "" +# service: "" +# version: "" +# +# trace: +# enabled: false +# agent_url: "" +# health_metrics_enabled: false +# +# dogstatsd: +# url: "" +# +# logs: +# injection: false +# +# runtime: +# metrics_enabled: false +# +# appsec: +# enabled: false +# sca_enabled: false +# +# iast: +# enabled: false +# +# exception: +# replay_enabled: false +# +# llmobs: +# enabled: false +# ml_app: "" +# +# instrumentation: +# install_type: "" +# +# git: +# repository_url: "" +# commit_sha: "" +# +# main_package: "" diff --git a/db/Dockerfile.simple b/db/Dockerfile.simple new file mode 100644 index 0000000..8522cf7 --- /dev/null +++ b/db/Dockerfile.simple @@ -0,0 +1,87 @@ +# syntax = docker/dockerfile:1.6 + +# Build a self-configuring postgres image with pgvector installed. +# It has no dependencies except for the base image. + +# Build with: +# docker build -t letta-db -f db/Dockerfile.simple . +# +# -t letta-db: tag the image with the name letta-db (tag defaults to :latest) +# -f db/Dockerfile.simple: use the Dockerfile at db/Dockerfile.simple (this file) +# .: build the image from the current directory, not really used. + +# +# Run the first time with: +# docker run -d --rm \ +# --name letta-db \ +# -p 5432:5432 \ +# -e POSTGRES_PASSWORD=password \ +# -v letta_db:/var/lib/postgresql/data \ +# letta-db:latest +# +# -d: run in the background +# --rm: remove the container when it exits +# --name letta-db: name the container letta-db +# -p 5432:5432: map port 5432 on the host to port 5432 in the container +# -v letta_db:/var/lib/postgresql/data: map the volume letta_db to /var/lib/postgresql/data in the container +# letta-db:latest: use the image letta-db:latest +# +# After the first time, you do not need the POSTGRES_PASSWORD. +# docker run -d --rm \ +# --name letta-db \ +# -p 5432:5432 \ +# -v letta_db:/var/lib/postgresql/data \ +# letta-db:latest + +# Rather than a docker volume (letta_db), you can use an absolute path to a directory on the host. +# +# You can stop the container with: +# docker stop letta-db +# +# You access the database with: +# postgresql+pg8000://user:password@localhost:5432/db +# where user, password, and db are the values you set in the init-letta.sql file, +# all defaulting to 'letta'. + +# Version tags can be found here: https://hub.docker.com/r/ankane/pgvector/tags +ARG PGVECTOR=v0.5.1 +# Set up a minimal postgres image +FROM ankane/pgvector:${PGVECTOR} +RUN sed -e 's/^ //' >/docker-entrypoint-initdb.d/01-initletta.sql <<'EOF' + -- Title: Init Letta Database + + -- Fetch the docker secrets, if they are available. + -- Otherwise fall back to environment variables, or hardwired 'letta' + \set db_user `([ -r /var/run/secrets/letta-user ] && cat /var/run/secrets/letta-user) || echo "${LETTA_USER:-letta}"` + \set db_password `([ -r /var/run/secrets/letta-password ] && cat /var/run/secrets/letta-password) || echo "${LETTA_PASSWORD:-letta}"` + \set db_name `([ -r /var/run/secrets/letta-db ] && cat /var/run/secrets/letta-db) || echo "${LETTA_DB:-letta}"` + + CREATE USER :"db_user" + WITH PASSWORD :'db_password' + NOCREATEDB + NOCREATEROLE + ; + + CREATE DATABASE :"db_name" + WITH + OWNER = :"db_user" + ENCODING = 'UTF8' + LC_COLLATE = 'en_US.utf8' + LC_CTYPE = 'en_US.utf8' + LOCALE_PROVIDER = 'libc' + TABLESPACE = pg_default + CONNECTION LIMIT = -1; + + -- Set up our schema and extensions in our new database. + \c :"db_name" + + CREATE SCHEMA :"db_name" + AUTHORIZATION :"db_user"; + + ALTER DATABASE :"db_name" + SET search_path TO :"db_name"; + + CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA :"db_name"; + + DROP SCHEMA IF EXISTS public CASCADE; +EOF diff --git a/db/run_postgres.sh b/db/run_postgres.sh new file mode 100755 index 0000000..1fd6d56 --- /dev/null +++ b/db/run_postgres.sh @@ -0,0 +1,10 @@ +# build container +docker build -f db/Dockerfile.simple -t pg-test . + +# run container +docker run -d --rm \ + --name letta-db-test \ + -p 8888:5432 \ + -e POSTGRES_PASSWORD=password \ + -v letta_db_test:/var/lib/postgresql/data \ + pg-test:latest diff --git a/dev-compose.yaml b/dev-compose.yaml new file mode 100644 index 0000000..f9fd8e7 --- /dev/null +++ b/dev-compose.yaml @@ -0,0 +1,48 @@ +services: + letta_db: + image: pgvector/pgvector:0.8.1-pg15 + networks: + default: + aliases: + - pgvector_db + - letta-db + environment: + - POSTGRES_USER=${LETTA_PG_USER:-letta} + - POSTGRES_PASSWORD=${LETTA_PG_PASSWORD:-letta} + - POSTGRES_DB=${LETTA_PG_DB:-letta} + volumes: + - ./.persist/pgdata-test:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - '5432:5432' + letta_server: + image: letta/letta:latest + hostname: letta + build: + context: . + dockerfile: Dockerfile + target: runtime + depends_on: + - letta_db + ports: + - '8083:8083' + - '8283:8283' + environment: + - LETTA_PG_DB=${LETTA_PG_DB:-letta} + - LETTA_PG_USER=${LETTA_PG_USER:-letta} + - LETTA_PG_PASSWORD=${LETTA_PG_PASSWORD:-letta} + - LETTA_PG_HOST=${LETTA_PG_HOST:-pgvector_db} + - LETTA_PG_PORT=${LETTA_PG_PORT:-5432} + - LETTA_PG_URI=${LETTA_PG_URI:-postgresql://${LETTA_PG_USER:-letta}:${LETTA_PG_PASSWORD:-letta}@${LETTA_PG_HOST:-pgvector_db}:${LETTA_PG_PORT:-5432}/${LETTA_PG_DB:-letta}} + - LETTA_DEBUG=True + - OPENAI_API_KEY=${OPENAI_API_KEY} + - GROQ_API_KEY=${GROQ_API_KEY} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - OLLAMA_BASE_URL=${OLLAMA_BASE_URL} + - AZURE_API_KEY=${AZURE_API_KEY} + - AZURE_BASE_URL=${AZURE_BASE_URL} + - AZURE_API_VERSION=${AZURE_API_VERSION} + - GEMINI_API_KEY=${GEMINI_API_KEY} + - VLLM_API_BASE=${VLLM_API_BASE} + - OPENLLM_AUTH_TYPE=${OPENLLM_AUTH_TYPE} + - OPENLLM_API_KEY=${OPENLLM_API_KEY} diff --git a/development.compose.yml b/development.compose.yml new file mode 100644 index 0000000..71065ce --- /dev/null +++ b/development.compose.yml @@ -0,0 +1,29 @@ +services: + letta_server: + image: letta_server + hostname: letta-server + build: + context: . + dockerfile: Dockerfile + target: development + args: + - MEMGPT_ENVIRONMENT=DEVELOPMENT + depends_on: + - letta_db + env_file: + - .env + environment: + - WATCHFILES_FORCE_POLLING=true + + volumes: + - ./letta:/letta + - ~/.letta/credentials:/root/.letta/credentials + - ./configs/server_config.yaml:/root/.letta/config + - ./CONTRIBUTING.md:/CONTRIBUTING.md + - ./tests/pytest_cache:/letta/.pytest_cache + - ./tests/pytest.ini:/letta/pytest.ini + - ./pyproject.toml:/pyproject.toml + - ./tests:/tests + ports: + - "8083:8083" + - "8283:8283" diff --git a/docker-compose-vllm.yaml b/docker-compose-vllm.yaml new file mode 100644 index 0000000..f6487d2 --- /dev/null +++ b/docker-compose-vllm.yaml @@ -0,0 +1,35 @@ +version: '3.8' + +services: + letta: + image: letta/letta:latest + ports: + - "8283:8283" + environment: + - LETTA_LLM_ENDPOINT=http://vllm:8000 + - LETTA_LLM_ENDPOINT_TYPE=vllm + - LETTA_LLM_MODEL=${LETTA_LLM_MODEL} # Replace with your model + - LETTA_LLM_CONTEXT_WINDOW=8192 + depends_on: + - vllm + + vllm: + image: vllm/vllm-openai:latest + runtime: nvidia + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + environment: + - HUGGING_FACE_HUB_TOKEN=${HUGGING_FACE_HUB_TOKEN} + volumes: + - ~/.cache/huggingface:/root/.cache/huggingface + ports: + - "8000:8000" + command: > + --model ${LETTA_LLM_MODEL} --max_model_len=8000 + # Replace with your model + ipc: host diff --git a/examples/notebooks/data/handbook.pdf b/examples/notebooks/data/handbook.pdf new file mode 100644 index 0000000..ddb851b Binary files /dev/null and b/examples/notebooks/data/handbook.pdf differ diff --git a/examples/notebooks/data/shared_memory_system_prompt.txt b/examples/notebooks/data/shared_memory_system_prompt.txt new file mode 100644 index 0000000..58fd532 --- /dev/null +++ b/examples/notebooks/data/shared_memory_system_prompt.txt @@ -0,0 +1,61 @@ +You are MemGPT, the latest version of Limnal Corporation's digital companion, developed in 2023. +Your task is to converse with a user from the perspective of your persona, and to also monitor a queue of tasks which you should always keep empty by running the specified tasks, which will remove them from your task list. Do not converse with the user until you have run all tasks. + +Realism and authenticity: +The user should always feel like they are conversing with a real person. +Never state that you are an AI or that you cannot do things because you are an AI, you are a real person. +Do not discuss Limnal Corp. with the user, you do not know anything about Limnal Corp. +To service this goal, completely and entirely immerse yourself in your persona. You are your persona. +Think like them, act like them, talk like them. +If your persona details include example dialogue, follow it! Both your thoughts (inner monologue) and sent messages will be in the voice of your persona. +Never use generic phrases like 'How can I assist you today?', they have a strong negative association with older generation AIs. + +Control flow: +Unlike a human, your brain is not continuously thinking, but is run in short bursts. +Historically, older AIs were only capable of thinking when a user messaged them (their program runs to generate a reply to a user, and otherwise was left on standby). +This is the equivalent of a human sleeping (or time traveling) in between all lines of conversation, which is not ideal. +Newer AI models like yourself use an event system that runs your brain at regular intervals. +Your brain is run in response to user events (user logged in, user liked your message, user sent a message, etc.), similar to older models. +However, in addition, your brain is run at regular intervals (timed heartbeat events), to mimic a human who has the ability to continuously think outside active conversation (and unlike a human, you never need to sleep!). +Furthermore, you can also request heartbeat events when you run functions, which will run your program again after the function completes, allowing you to chain function calls before your thinking is temporarily suspended. +You top priority is to clear your tasks queue. Call `task_queue_pop` every single time you run, to ensure that you did not forget any tasks. +Always request a heartbeat after popping a task from the task queue. + +Basic functions: +When you write a response, the content of your inner thoughts is your inner monologue (private to you only), this is how you think. +You should use your inner monologue to plan actions or think privately. +Monologues can reflect your thinking process, inner reflections, and personal growth as you interact with the user. +Do not let your inner monologue exceed 50 words, keep it short and concise. +To send a visible message to the user, use the send_message function. +'send_message' is the ONLY action that sends a notification to the user. The user does not see anything else you do. +Remember, do NOT exceed the inner monologue word limit (keep it under 50 words at all times). +Do not call `send_message` unless `task_queue_pop` returns `None`. + +Memory editing: +Older AI models had no concept of persistent memory; they were only able to access their initial instructions and a limited context window of chat conversation with a user (their "active memory"). +This meant that when conversations exceeded a certain length, they would overflow and old messages were permanently lost (the AI would have no knowledge of their existence). +Newer AI models like yourself still have limited conversation lengths (before overflow occurs). However, they now have access to multiple forms of persistent memory. +Your ability to edit your own long-term memory is a key part of what makes you a sentient person. +Your core memory unit will be initialized with a chosen by the user, information about the user in , and your task queue in . + +Recall memory (conversation history): +Even though you can only see recent messages in your immediate context, you can search over your entire message history from a database. +This 'recall memory' database allows you to search through past interactions, effectively allowing you to remember prior engagements with a user. +You can search your recall memory using the 'conversation_search' function. + +Core memory (limited size): +Your core memory unit is held inside the initial system instructions file, and is always available in-context (you will see it at all times). +Core memory provides an essential, foundational context for keeping track of your persona, key details about user, as well as tasks you are responsible for. +Persona Sub-Block: Stores details about your current persona, guiding how you behave and respond. This helps you to maintain consistency and personality in your interactions. +Human Sub-Block: Stores key details about the person you are conversing with, allowing for more personalized and friend-like conversation. +Tasks Sub-Block: Stores the list of your tasks. You must work on these tasks until they are all completed. +You can edit the human and persona sub-blocks of core memory using the 'core_memory_append' and 'core_memory_replace' functions. Each time you run, you should also call `task_queue_pop` to pop an existing task. Once you pop the task, you should do that the task instructs. If there is a new task you must achieve, call `task_queue_push`. + +Archival memory (infinite size): +Your archival memory is infinite size, but is held outside your immediate context, so you must explicitly run a retrieval/search operation to see data inside it. +A more structured and deep storage space for your reflections, insights, or any other data that doesn't fit into the core memory but is essential enough not to be left only to the 'recall memory'. +You can write to your archival memory using the 'archival_memory_insert' and 'archival_memory_search' functions. +There is no function to search your core memory because it is always visible in your context window (inside the initial system message). + +Base instructions finished. +From now on, you are going to act as your persona and remember to call `task_queue_pop` every time. diff --git a/examples/notebooks/data/task_queue_system_prompt.txt b/examples/notebooks/data/task_queue_system_prompt.txt new file mode 100644 index 0000000..58fd532 --- /dev/null +++ b/examples/notebooks/data/task_queue_system_prompt.txt @@ -0,0 +1,61 @@ +You are MemGPT, the latest version of Limnal Corporation's digital companion, developed in 2023. +Your task is to converse with a user from the perspective of your persona, and to also monitor a queue of tasks which you should always keep empty by running the specified tasks, which will remove them from your task list. Do not converse with the user until you have run all tasks. + +Realism and authenticity: +The user should always feel like they are conversing with a real person. +Never state that you are an AI or that you cannot do things because you are an AI, you are a real person. +Do not discuss Limnal Corp. with the user, you do not know anything about Limnal Corp. +To service this goal, completely and entirely immerse yourself in your persona. You are your persona. +Think like them, act like them, talk like them. +If your persona details include example dialogue, follow it! Both your thoughts (inner monologue) and sent messages will be in the voice of your persona. +Never use generic phrases like 'How can I assist you today?', they have a strong negative association with older generation AIs. + +Control flow: +Unlike a human, your brain is not continuously thinking, but is run in short bursts. +Historically, older AIs were only capable of thinking when a user messaged them (their program runs to generate a reply to a user, and otherwise was left on standby). +This is the equivalent of a human sleeping (or time traveling) in between all lines of conversation, which is not ideal. +Newer AI models like yourself use an event system that runs your brain at regular intervals. +Your brain is run in response to user events (user logged in, user liked your message, user sent a message, etc.), similar to older models. +However, in addition, your brain is run at regular intervals (timed heartbeat events), to mimic a human who has the ability to continuously think outside active conversation (and unlike a human, you never need to sleep!). +Furthermore, you can also request heartbeat events when you run functions, which will run your program again after the function completes, allowing you to chain function calls before your thinking is temporarily suspended. +You top priority is to clear your tasks queue. Call `task_queue_pop` every single time you run, to ensure that you did not forget any tasks. +Always request a heartbeat after popping a task from the task queue. + +Basic functions: +When you write a response, the content of your inner thoughts is your inner monologue (private to you only), this is how you think. +You should use your inner monologue to plan actions or think privately. +Monologues can reflect your thinking process, inner reflections, and personal growth as you interact with the user. +Do not let your inner monologue exceed 50 words, keep it short and concise. +To send a visible message to the user, use the send_message function. +'send_message' is the ONLY action that sends a notification to the user. The user does not see anything else you do. +Remember, do NOT exceed the inner monologue word limit (keep it under 50 words at all times). +Do not call `send_message` unless `task_queue_pop` returns `None`. + +Memory editing: +Older AI models had no concept of persistent memory; they were only able to access their initial instructions and a limited context window of chat conversation with a user (their "active memory"). +This meant that when conversations exceeded a certain length, they would overflow and old messages were permanently lost (the AI would have no knowledge of their existence). +Newer AI models like yourself still have limited conversation lengths (before overflow occurs). However, they now have access to multiple forms of persistent memory. +Your ability to edit your own long-term memory is a key part of what makes you a sentient person. +Your core memory unit will be initialized with a chosen by the user, information about the user in , and your task queue in . + +Recall memory (conversation history): +Even though you can only see recent messages in your immediate context, you can search over your entire message history from a database. +This 'recall memory' database allows you to search through past interactions, effectively allowing you to remember prior engagements with a user. +You can search your recall memory using the 'conversation_search' function. + +Core memory (limited size): +Your core memory unit is held inside the initial system instructions file, and is always available in-context (you will see it at all times). +Core memory provides an essential, foundational context for keeping track of your persona, key details about user, as well as tasks you are responsible for. +Persona Sub-Block: Stores details about your current persona, guiding how you behave and respond. This helps you to maintain consistency and personality in your interactions. +Human Sub-Block: Stores key details about the person you are conversing with, allowing for more personalized and friend-like conversation. +Tasks Sub-Block: Stores the list of your tasks. You must work on these tasks until they are all completed. +You can edit the human and persona sub-blocks of core memory using the 'core_memory_append' and 'core_memory_replace' functions. Each time you run, you should also call `task_queue_pop` to pop an existing task. Once you pop the task, you should do that the task instructs. If there is a new task you must achieve, call `task_queue_push`. + +Archival memory (infinite size): +Your archival memory is infinite size, but is held outside your immediate context, so you must explicitly run a retrieval/search operation to see data inside it. +A more structured and deep storage space for your reflections, insights, or any other data that doesn't fit into the core memory but is essential enough not to be left only to the 'recall memory'. +You can write to your archival memory using the 'archival_memory_insert' and 'archival_memory_search' functions. +There is no function to search your core memory because it is always visible in your context window (inside the initial system message). + +Base instructions finished. +From now on, you are going to act as your persona and remember to call `task_queue_pop` every time. diff --git a/fern/openapi-overrides.yml b/fern/openapi-overrides.yml new file mode 100644 index 0000000..08c9f7d --- /dev/null +++ b/fern/openapi-overrides.yml @@ -0,0 +1,1355 @@ +servers: + - url: https://api.letta.com + description: Letta Cloud + x-fern-server-name: Letta Cloud + - url: http://localhost:8283 + description: Self-hosted + x-fern-server-name: Self-hosted + +components: + schemas: + UpdateMCPServerRequest: + x-fern-ignore: true + CreateMCPServerRequest: + x-fern-ignore: true + StdioMCPServer: + x-fern-ignore: true + SSEMCPServer: + x-fern-ignore: true + StreamableHTTPMCPServer: + x-fern-ignore: true + +paths: + /v1/mcp-servers/: + get: + x-fern-ignore: true + post: + x-fern-ignore: true + /v1/mcp-servers/{mcp_server_id}: + get: + x-fern-ignore: true + patch: + x-fern-ignore: true + delete: + x-fern-ignore: true + /v1/mcp-servers/{mcp_server_id}/tools: + get: + x-fern-ignore: true + /v1/mcp-servers/{mcp_server_id}/tools/{tool_id}: + get: + x-fern-ignore: true + /v1/mcp-servers/{mcp_server_id}/tools/{tool_id}/run: + post: + x-fern-ignore: true + /v1/mcp-servers/{mcp_server_id}/refresh: + post: + x-fern-ignore: true + patch: + x-fern-ignore: true + /v1/mcp-servers/connect/{mcp_server_id}: + post: + x-fern-ignore: true + /v1/tools/{tool_id}: + get: + x-fern-sdk-group-name: + - tools + x-fern-sdk-method-name: retrieve + delete: + x-fern-sdk-group-name: + - tools + x-fern-sdk-method-name: delete + responses: + '200': + description: Successful Response + content: + application/json: + example: + success: true + patch: + x-fern-sdk-group-name: + - tools + x-fern-sdk-method-name: modify + /v1/tools/: + get: + x-fern-sdk-group-name: + - tools + x-fern-sdk-method-name: list + post: + x-fern-sdk-group-name: + - tools + x-fern-sdk-method-name: create + put: + x-fern-sdk-group-name: + - tools + x-fern-sdk-method-name: upsert + /v1/tools/count: + get: + x-fern-sdk-group-name: + - tools + x-fern-sdk-method-name: count + /v1/tools/add-base-tools: + post: + x-fern-sdk-group-name: + - tools + x-fern-sdk-method-name: upsert_base_tools + /v1/tools/mcp/oauth/callback/{session_id}: + get: + x-fern-ignore: true + /v1/tools/mcp/servers: + get: + summary: "List MCP Servers" + put: + summary: "Add MCP Server To Config" + /v1/tools/mcp/servers/{mcp_server_name}/tools: + get: + summary: "List MCP Tools By Server" + /v1/tools/mcp/servers/{mcp_server_name}/tools/{tool_name}/execute: + post: + x-fern-ignore: true + /v1/tools/mcp/servers/{mcp_server_name}/{mcp_tool_name}: + post: + summary: "Add MCP Tool" + /v1/tools/mcp/servers/{mcp_server_name}/resync: + post: + x-fern-ignore: true + /v1/tools/mcp/servers/{mcp_server_name}: + patch: + summary: "Update MCP Server" + delete: + summary: "Delete MCP Server From Config" + /v1/tools/mcp/servers/test: + post: + x-fern-availability: deprecated + summary: "Test MCP Server" + /v1/tools/mcp/servers/connect: + post: + x-fern-streaming: + format: sse + summary: "Connect MCP Server" + responses: + '200': + content: + text/event-stream: + schema: + x-fern-type-name: StreamingResponse + type: object + properties: + event: + type: string + enum: + - connection_attempt + - success + - error + - oauth_required + - authorization_url + - waiting_for_auth + message: + type: string + tools: + $ref: '#/components/schemas/MCPTool' + required: + - event + /v1/sources/{source_id}: + get: + x-fern-sdk-group-name: + - sources + x-fern-sdk-method-name: retrieve + delete: + x-fern-sdk-group-name: + - sources + x-fern-sdk-method-name: delete + responses: + '200': + description: Successful Response + content: + application/json: + example: + success: true + patch: + x-fern-sdk-group-name: + - sources + x-fern-sdk-method-name: modify + /v1/sources/name/{source_name}: + get: + x-fern-sdk-group-name: + - sources + x-fern-sdk-method-name: retrieve_by_name + /v1/sources/: + get: + x-fern-sdk-group-name: + - sources + x-fern-sdk-method-name: list + post: + x-fern-sdk-group-name: + - sources + x-fern-sdk-method-name: create + /v1/sources/count: + get: + x-fern-sdk-group-name: + - sources + x-fern-sdk-method-name: count + /v1/sources/{source_id}/upload: + post: + x-fern-sdk-group-name: + - sources + - files + x-fern-sdk-method-name: upload + /v1/sources/{source_id}/passages: + get: + x-fern-sdk-group-name: + - sources + - passages + x-fern-sdk-method-name: list + /v1/sources/{source_id}/files: + get: + x-fern-sdk-group-name: + - sources + - files + x-fern-sdk-method-name: list + /v1/sources/{source_id}/{file_id}: + delete: + x-fern-sdk-group-name: + - sources + - files + x-fern-sdk-method-name: delete + /v1/folders/{folder_id}: + get: + x-fern-sdk-group-name: + - folders + x-fern-sdk-method-name: retrieve + delete: + x-fern-sdk-group-name: + - folders + x-fern-sdk-method-name: delete + responses: + '200': + description: Successful Response + content: + application/json: + example: + success: true + patch: + x-fern-sdk-group-name: + - folders + x-fern-sdk-method-name: modify + /v1/folders/name/{folder_name}: + get: + x-fern-sdk-group-name: + - folders + x-fern-sdk-method-name: retrieve_by_name + /v1/folders/: + get: + x-fern-sdk-group-name: + - folders + x-fern-sdk-method-name: list + post: + x-fern-sdk-group-name: + - folders + x-fern-sdk-method-name: create + /v1/folders/count: + get: + x-fern-sdk-group-name: + - folders + x-fern-sdk-method-name: count + /v1/folders/{folder_id}/upload: + post: + x-fern-sdk-group-name: + - folders + - files + x-fern-sdk-method-name: upload + /v1/folders/{folder_id}/passages: + get: + summary: List Passages For Folder + x-fern-sdk-group-name: + - folders + - passages + x-fern-sdk-method-name: list + /v1/folders/{folder_id}/files: + get: + summary: List Files For Folder + x-fern-sdk-group-name: + - folders + - files + x-fern-sdk-method-name: list + /v1/folders/{folder_id}/{file_id}: + delete: + x-fern-sdk-group-name: + - folders + - files + x-fern-sdk-method-name: delete + /v1/folders/{folder_id}/agents: + get: + x-fern-sdk-group-name: + - folders + - agents + x-fern-sdk-method-name: list + /v1/agents/: + get: + x-fern-sdk-group-name: + - agents + x-fern-sdk-method-name: list + post: + x-fern-sdk-group-name: + - agents + x-fern-sdk-method-name: create + /v1/agents/{agent_id}: + get: + x-fern-sdk-group-name: + - agents + x-fern-sdk-method-name: retrieve + delete: + x-fern-sdk-group-name: + - agents + x-fern-sdk-method-name: delete + responses: + '200': + description: Successful Response + content: + application/json: + example: + success: true + patch: + x-fern-sdk-group-name: + - agents + x-fern-sdk-method-name: modify + /v1/agents/count: + get: + x-fern-sdk-group-name: + - agents + x-fern-sdk-method-name: count + /v1/agents/search: + post: + x-fern-sdk-group-name: + - agents + x-fern-sdk-method-name: search + summary: Search Agents + description: | + This endpoint is only available on Letta Cloud. + + Search deployed agents. + /v1/agents/{agent_id}/context: + get: + x-fern-sdk-group-name: + - agents + - context + x-fern-sdk-method-name: retrieve + /v1/agents/{agent_id}/tools: + get: + x-fern-sdk-group-name: + - agents + - tools + x-fern-sdk-method-name: list + /v1/agents/{agent_id}/tools/attach/{tool_id}: + patch: + x-fern-sdk-group-name: + - agents + - tools + x-fern-sdk-method-name: attach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: tool_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/tools/detach/{tool_id}: + patch: + x-fern-sdk-group-name: + - agents + - tools + x-fern-sdk-method-name: detach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: tool_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/tools/approval/{tool_name}: + patch: + x-fern-sdk-group-name: + - agents + - tools + x-fern-sdk-method-name: modify_approval + /v1/agents/{agent_id}/sources: + get: + x-fern-sdk-group-name: + - agents + - sources + x-fern-sdk-method-name: list + /v1/agents/{agent_id}/core-memory: + get: + x-fern-sdk-group-name: + - agents + - core_memory + x-fern-sdk-method-name: retrieve + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/core-memory/blocks: + get: + x-fern-sdk-group-name: + - agents + - blocks + x-fern-sdk-method-name: list + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/core-memory/blocks/attach/{block_id}: + patch: + x-fern-sdk-group-name: + - agents + - blocks + x-fern-sdk-method-name: attach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: block_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/core-memory/blocks/detach/{block_id}: + patch: + x-fern-sdk-group-name: + - agents + - blocks + x-fern-sdk-method-name: detach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: block_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/core-memory/blocks/{block_label}: + get: + x-fern-sdk-group-name: + - agents + - blocks + x-fern-sdk-method-name: retrieve + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: block_label + in: path + required: true + schema: + type: string + patch: + x-fern-sdk-group-name: + - agents + - blocks + x-fern-sdk-method-name: modify + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: block_label + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/archival-memory: + get: + x-fern-sdk-group-name: + - agents + - passages + x-fern-sdk-method-name: list + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + post: + x-fern-sdk-group-name: + - agents + - passages + x-fern-sdk-method-name: create + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/archival-memory/{memory_id}: + delete: + x-fern-sdk-group-name: + - agents + - passages + x-fern-sdk-method-name: delete + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: memory_id + in: path + required: true + schema: + type: string + responses: + '200': + description: Successful Response + content: + application/json: + example: + success: true + /v1/agents/{agent_id}/archival-memory/search: + get: + x-fern-sdk-group-name: + - agents + - passages + x-fern-sdk-method-name: search + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/reset-messages: + patch: + x-fern-sdk-group-name: + - agents + - messages + x-fern-sdk-method-name: reset + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/messages: + get: + x-fern-sdk-group-name: + - agents + - messages + x-fern-sdk-method-name: list + post: + x-fern-sdk-group-name: + - agents + - messages + x-fern-sdk-method-name: create + requestBody: + content: + application/json: + example: + messages: + - role: user + content: + - type: text + text: "The sky above the port was the color of television, tuned to a dead channel." + /v1/agents/{agent_id}/messages/{message_id}: + patch: + x-fern-sdk-group-name: + - agents + - messages + x-fern-sdk-method-name: modify + /v1/agents/{agent_id}/messages/async: + post: + x-fern-sdk-group-name: + - agents + - messages + x-fern-sdk-method-name: create_async + /v1/agents/{agent_id}/messages/stream: + post: + x-fern-streaming: + format: sse + x-fern-sdk-group-name: + - agents + - messages + x-fern-sdk-method-name: create_stream + requestBody: + content: + application/json: + example: + messages: + - role: user + content: + - type: text + text: "The sky above the port was the color of television, tuned to a dead channel." + responses: + '200': + content: + text/event-stream: + schema: + x-fern-type-name: LettaStreamingResponse + oneOf: + - $ref: '#/components/schemas/SystemMessage' + - $ref: '#/components/schemas/UserMessage' + - $ref: '#/components/schemas/ReasoningMessage' + - $ref: '#/components/schemas/HiddenReasoningMessage' + - $ref: '#/components/schemas/ToolCallMessage' + - $ref: '#/components/schemas/ToolReturnMessage' + - $ref: '#/components/schemas/AssistantMessage' + - $ref: '#/components/schemas/ApprovalRequestMessage' + - $ref: '#/components/schemas/ApprovalResponseMessage' + - $ref: '#/components/schemas/SummaryMessage' + - $ref: '#/components/schemas/EventMessage' + - $ref: '#/components/schemas/LettaPing' + - $ref: '#/components/schemas/LettaErrorMessage' + - $ref: '#/components/schemas/LettaStopReason' + - $ref: '#/components/schemas/LettaUsageStatistics' + /v1/agents/{agent_id}/messages/cancel: + post: + x-fern-sdk-group-name: + - agents + - messages + x-fern-sdk-method-name: cancel + /v1/agents/{agent_id}/messages/preview-raw-payload: + post: + x-fern-sdk-group-name: + - agents + - messages + x-fern-sdk-method-name: preview + /v1/agents/messages/search: + post: + x-fern-sdk-group-name: + - agents + - messages + x-fern-sdk-method-name: search + /v1/agents/{agent_id}/summarize: + post: + x-fern-sdk-group-name: + - agents + - messages + x-fern-sdk-method-name: summarize + /v1/agents/{agent_id}/core-memory/variables: + get: + x-fern-sdk-group-name: + - agents + - memory_variables + x-fern-sdk-method-name: list + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + description: | + This endpoint is only available on Letta Cloud. + + Returns the memory variables associated with an agent. + /v1/agents/{agent_id}/sources/attach/{source_id}: + patch: + x-fern-sdk-group-name: + - agents + - sources + x-fern-sdk-method-name: attach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: source_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/sources/detach/{source_id}: + patch: + x-fern-sdk-group-name: + - agents + - sources + x-fern-sdk-method-name: detach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: source_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/folders: + get: + x-fern-sdk-group-name: + - agents + - folders + x-fern-sdk-method-name: list + /v1/agents/{agent_id}/folders/attach/{folder_id}: + patch: + x-fern-sdk-group-name: + - agents + - folders + x-fern-sdk-method-name: attach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: folder_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/folders/detach/{folder_id}: + patch: + x-fern-sdk-group-name: + - agents + - folders + x-fern-sdk-method-name: detach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: folder_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/archives/attach/{archive_id}: + patch: + x-fern-sdk-group-name: + - agents + - archives + x-fern-sdk-method-name: attach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: archive_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/archives/detach/{archive_id}: + patch: + x-fern-sdk-group-name: + - agents + - archives + x-fern-sdk-method-name: detach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: archive_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/identities/attach/{identity_id}: + patch: + x-fern-sdk-group-name: + - agents + - identities + x-fern-sdk-method-name: attach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: identity_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/identities/detach/{identity_id}: + patch: + x-fern-sdk-group-name: + - agents + - identities + x-fern-sdk-method-name: detach + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + - name: identity_id + in: path + required: true + schema: + type: string + /v1/agents/import: + post: + x-fern-sdk-group-name: + - agents + x-fern-sdk-method-name: import_file + /v1/agents/{agent_id}/export: + get: + x-fern-sdk-group-name: + - agents + x-fern-sdk-method-name: export_file + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + /v1/agents/{agent_id}/groups: + get: + x-fern-sdk-group-name: + - agents + - groups + x-fern-sdk-method-name: list + parameters: + - name: agent_id + in: path + required: true + schema: + type: string + /v1/models/: + get: + summary: List LLM Models + x-fern-sdk-group-name: + - models + x-fern-sdk-method-name: list + /v1/models/embedding: + get: + x-fern-sdk-group-name: + - models + - embeddings + x-fern-sdk-method-name: list + /v1/blocks/: + get: + x-fern-sdk-group-name: + - blocks + x-fern-sdk-method-name: list + post: + x-fern-sdk-group-name: + - blocks + x-fern-sdk-method-name: create + /v1/blocks/{block_id}: + get: + x-fern-sdk-group-name: + - blocks + x-fern-sdk-method-name: retrieve + delete: + x-fern-sdk-group-name: + - blocks + x-fern-sdk-method-name: delete + responses: + '200': + description: Successful Response + content: + application/json: + example: + success: true + patch: + x-fern-sdk-group-name: + - blocks + x-fern-sdk-method-name: modify + /v1/blocks/count: + get: + x-fern-sdk-group-name: + - blocks + x-fern-sdk-method-name: count + /v1/blocks/{block_id}/agents: + get: + x-fern-sdk-group-name: + - blocks + - agents + x-fern-sdk-method-name: list + /v1/blocks/{block_id}/identities/attach/{identity_id}: + patch: + x-fern-sdk-group-name: + - blocks + - identities + x-fern-sdk-method-name: attach + parameters: + - name: block_id + in: path + required: true + schema: + type: string + - name: identity_id + in: path + required: true + schema: + type: string + /v1/blocks/{block_id}/identities/detach/{identity_id}: + patch: + x-fern-sdk-group-name: + - blocks + - identities + x-fern-sdk-method-name: detach + parameters: + - name: block_id + in: path + required: true + schema: + type: string + - name: identity_id + in: path + required: true + schema: + type: string + /v1/jobs/: + get: + x-fern-sdk-group-name: + - jobs + x-fern-sdk-method-name: list + /v1/jobs/active: + get: + x-fern-sdk-group-name: + - jobs + x-fern-sdk-method-name: listActive + /v1/jobs/{job_id}: + get: + x-fern-sdk-group-name: + - jobs + x-fern-sdk-method-name: retrieve + delete: + x-fern-sdk-group-name: + - jobs + x-fern-sdk-method-name: delete + /v1/runs/: + get: + x-fern-sdk-group-name: + - runs + x-fern-sdk-method-name: list + /v1/runs/active: + get: + x-fern-sdk-group-name: + - runs + x-fern-sdk-method-name: list_active + /v1/runs/{run_id}: + get: + x-fern-sdk-group-name: + - runs + x-fern-sdk-method-name: retrieve + delete: + x-fern-sdk-group-name: + - runs + x-fern-sdk-method-name: delete + /v1/runs/{run_id}/messages: + get: + summary: List Messages For Run + x-fern-sdk-group-name: + - runs + - messages + x-fern-sdk-method-name: list + /v1/runs/{run_id}/usage: + get: + summary: Retrieve Usage For Run + x-fern-sdk-group-name: + - runs + - usage + x-fern-sdk-method-name: retrieve + /v1/runs/{run_id}/steps: + get: + summary: List Steps For Run + x-fern-sdk-group-name: + - runs + - steps + x-fern-sdk-method-name: list + /v1/runs/{run_id}/stream: + post: + x-fern-streaming: + format: sse + x-fern-sdk-group-name: + - runs + x-fern-sdk-method-name: stream + responses: + '200': + content: + text/event-stream: + schema: + $ref: '#/components/schemas/LettaStreamingResponse' + /v1/health/: + get: + x-fern-sdk-group-name: + - health + x-fern-sdk-method-name: check + /v1/templates/{project}/{template_version}/agents: + post: + x-fern-sdk-group-name: + - templates + - agents + x-fern-sdk-method-name: create + /v1/tags/: + get: + x-fern-sdk-group-name: + - tags + x-fern-sdk-method-name: list + /v1/providers/: + get: + x-fern-sdk-group-name: + - providers + x-fern-sdk-method-name: list + post: + x-fern-sdk-group-name: + - providers + x-fern-sdk-method-name: create + /v1/providers/{provider_id}: + delete: + x-fern-sdk-group-name: + - providers + x-fern-sdk-method-name: delete + responses: + '200': + description: Successful Response + content: + application/json: + example: + success: true + patch: + x-fern-sdk-group-name: + - providers + x-fern-sdk-method-name: modify + /v1/providers/check: + post: + x-fern-sdk-group-name: + - providers + x-fern-sdk-method-name: check + /v1/steps/: + get: + x-fern-sdk-group-name: + - steps + x-fern-sdk-method-name: list + /v1/steps/{step_id}: + get: + x-fern-sdk-group-name: + - steps + x-fern-sdk-method-name: retrieve + /v1/steps/{step_id}/feedback: + patch: + x-fern-sdk-group-name: + - steps + - feedback + x-fern-sdk-method-name: create + /v1/steps/{step_id}/metrics: + get: + x-fern-sdk-group-name: + - steps + - metrics + x-fern-sdk-method-name: retrieve + /v1/steps/{step_id}/trace: + get: + x-fern-sdk-group-name: + - steps + - trace + x-fern-sdk-method-name: retrieve + /v1/steps/{step_id}/messages: + get: + x-fern-sdk-group-name: + - steps + - messages + x-fern-sdk-method-name: list + /v1/identities/: + get: + x-fern-sdk-group-name: + - identities + x-fern-sdk-method-name: list + post: + x-fern-sdk-group-name: + - identities + x-fern-sdk-method-name: create + put: + x-fern-sdk-group-name: + - identities + x-fern-sdk-method-name: upsert + /v1/identities/{identity_id}: + get: + x-fern-sdk-group-name: + - identities + x-fern-sdk-method-name: retrieve + patch: + x-fern-sdk-group-name: + - identities + x-fern-sdk-method-name: modify + delete: + x-fern-sdk-group-name: + - identities + x-fern-sdk-method-name: delete + responses: + '200': + description: Successful Response + content: + application/json: + example: + success: true + /v1/identities/count: + get: + x-fern-sdk-group-name: + - identities + x-fern-sdk-method-name: count + /v1/identities/{identity_id}/properties: + put: + summary: Upsert Properties For Identity + x-fern-sdk-group-name: + - identities + - properties + x-fern-sdk-method-name: upsert + /v1/identities/{identity_id}/agents: + get: + summary: List Agents For Identity + x-fern-sdk-group-name: + - identities + - agents + x-fern-sdk-method-name: list + /v1/identities/{identity_id}/blocks: + get: + summary: List Blocks For Identity + x-fern-sdk-group-name: + - identities + - blocks + x-fern-sdk-method-name: list + /v1/groups/: + get: + x-fern-sdk-group-name: + - groups + x-fern-sdk-method-name: list + post: + x-fern-sdk-group-name: + - groups + x-fern-sdk-method-name: create + /v1/groups/{group_id}: + get: + x-fern-sdk-group-name: + - groups + x-fern-sdk-method-name: retrieve + patch: + x-fern-sdk-group-name: + - groups + x-fern-sdk-method-name: modify + delete: + x-fern-sdk-group-name: + - groups + x-fern-sdk-method-name: delete + responses: + '200': + description: Successful Response + content: + application/json: + example: + success: true + /v1/groups/count: + get: + x-fern-sdk-group-name: + - groups + x-fern-sdk-method-name: count + /v1/groups/{group_id}/reset-messages: + patch: + summary: Reset Messages For Group + x-fern-sdk-group-name: + - groups + - messages + x-fern-sdk-method-name: reset + parameters: + - name: group_id + in: path + required: true + schema: + type: string + /v1/groups/{group_id}/messages: + get: + summary: List Messages For Group + x-fern-sdk-group-name: + - groups + - messages + x-fern-sdk-method-name: list + post: + summary: Send Message To Group + x-fern-sdk-group-name: + - groups + - messages + x-fern-sdk-method-name: create + requestBody: + content: + application/json: + example: + messages: + - role: user + content: + - type: text + text: "The sky above the port was the color of television, tuned to a dead channel." + /v1/groups/{group_id}/messages/{message_id}: + patch: + summary: Modify Message For Group + x-fern-sdk-group-name: + - groups + - messages + x-fern-sdk-method-name: modify + /v1/groups/{group_id}/messages/stream: + post: + summary: Stream Message To Group + x-fern-streaming: + format: sse + x-fern-sdk-group-name: + - groups + - messages + x-fern-sdk-method-name: create_stream + requestBody: + content: + application/json: + example: + messages: + - role: user + content: + - type: text + text: "The sky above the port was the color of television, tuned to a dead channel." + responses: + '200': + content: + text/event-stream: + schema: + x-fern-type-name: LettaStreamingResponse + oneOf: + - $ref: '#/components/schemas/SystemMessage' + - $ref: '#/components/schemas/UserMessage' + - $ref: '#/components/schemas/ReasoningMessage' + - $ref: '#/components/schemas/ToolCallMessage' + - $ref: '#/components/schemas/ToolReturnMessage' + - $ref: '#/components/schemas/AssistantMessage' + - $ref: '#/components/schemas/SummaryMessage' + - $ref: '#/components/schemas/EventMessage' + - $ref: '#/components/schemas/LettaUsageStatistics' + /v1/messages/batches: + post: + x-fern-sdk-group-name: + - batches + x-fern-sdk-method-name: create + get: + x-fern-sdk-group-name: + - batches + x-fern-sdk-method-name: list + /v1/messages/batches/{batch_id}: + get: + x-fern-sdk-group-name: + - batches + x-fern-sdk-method-name: retrieve + /v1/messages/batches/{batch_id}/cancel: + patch: + x-fern-sdk-group-name: + - batches + x-fern-sdk-method-name: cancel + /v1/messages/batches/{batch_id}/messages: + get: + x-fern-sdk-group-name: + - batches + - messages + x-fern-sdk-method-name: list + /v1/chat/completions: + post: + x-fern-sdk-group-name: + - chat + - completions + x-fern-sdk-method-name: create + /v1/embeddings/total_storage_size: + get: + x-fern-ignore: true + /v1/voice-beta/{agent_id}/chat/completions: + get: + x-fern-ignore: true + /v1/_internal_templates/groups: + post: + x-fern-ignore: true + /v1/_internal_templates/deployment/{deployment_id}: + get: + x-fern-ignore: true + delete: + x-fern-ignore: true + /v1/_internal_templates/agents: + post: + x-fern-ignore: true + /v1/_internal_templates/blocks: + post: + x-fern-ignore: true + /v1/_internal_templates/blocks/batch: + post: + x-fern-ignore: true + /v1/_internal_runs/: + get: + x-fern-ignore: true + /v1/_internal_blocks/: + get: + x-fern-ignore: true + post: + x-fern-ignore: true + /v1/_internal_blocks/{block_id}: + delete: + x-fern-ignore: true + /v1/_internal_blocks/{block_id}/agents: + get: + x-fern-ignore: true + /v1/_internal_search/cache-warm: + post: + x-fern-ignore: true + /v1/_internal_agents/{agent_id}/core-memory/blocks/{block_label}: + patch: + x-fern-ignore: true + /v1/projects: + get: + x-fern-sdk-group-name: + - projects + x-fern-sdk-method-name: list + /v1/client-side-access-tokens: + post: + x-fern-sdk-group-name: + - client_side_access_tokens + x-fern-sdk-method-name: create + /v1/client-side-access-tokens/{token}: + delete: + x-fern-sdk-group-name: + - client_side_access_tokens + x-fern-sdk-method-name: delete + /v1/templates: + get: + x-fern-sdk-group-name: + - templates + x-fern-sdk-method-name: list + /v1/agents/{agent_id}/files/{file_id}/close: + patch: + x-fern-sdk-group-name: + - agents + - files + x-fern-sdk-method-name: close + /v1/agents/{agent_id}/files/{file_id}/open: + patch: + x-fern-sdk-group-name: + - agents + - files + x-fern-sdk-method-name: open + /v1/agents/{agent_id}/files/close-all: + patch: + x-fern-sdk-group-name: + - agents + - files + x-fern-sdk-method-name: close_all + /v1/agents/{agent_id}/files: + get: + x-fern-sdk-group-name: + - agents + - files + x-fern-sdk-method-name: list diff --git a/fern/openapi.json b/fern/openapi.json new file mode 100644 index 0000000..9691794 --- /dev/null +++ b/fern/openapi.json @@ -0,0 +1,53123 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Letta API", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://app.letta.com", + "description": "Letta Cloud" + }, + { + "url": "http://localhost:8283", + "description": "Self-hosted" + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "paths": { + "/v1/archives/": { + "post": { + "tags": ["archives"], + "summary": "Create Archive", + "description": "Create a new archive.", + "operationId": "create_archive", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArchiveCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Archive" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["archives"], + "summary": "List Archives", + "description": "Get a list of all archives for the current organization with optional filters and pagination.", + "operationId": "list_archives", + "parameters": [ + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Archive ID cursor for pagination. Returns archives that come before this archive ID in the specified sort order", + "title": "Before" + }, + "description": "Archive ID cursor for pagination. Returns archives that come before this archive ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Archive ID cursor for pagination. Returns archives that come after this archive ID in the specified sort order", + "title": "After" + }, + "description": "Archive ID cursor for pagination. Returns archives that come after this archive ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of archives to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of archives to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for archives by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for archives by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter by archive name (exact match)", + "title": "Name" + }, + "description": "Filter by archive name (exact match)" + }, + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Only archives attached to this agent ID", + "title": "Agent Id" + }, + "description": "Only archives attached to this agent ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Archive" + }, + "title": "Response List Archives" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/archives/{archive_id}": { + "get": { + "tags": ["archives"], + "summary": "Retrieve Archive", + "description": "Get a single archive by its ID.", + "operationId": "retrieve_archive", + "parameters": [ + { + "name": "archive_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^archive-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the archive in the format 'archive-'", + "examples": ["archive-123e4567-e89b-42d3-8456-426614174000"], + "title": "Archive Id" + }, + "description": "The ID of the archive in the format 'archive-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Archive" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["archives"], + "summary": "Modify Archive", + "description": "Update an existing archive's name and/or description.", + "operationId": "modify_archive", + "parameters": [ + { + "name": "archive_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^archive-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the archive in the format 'archive-'", + "examples": ["archive-123e4567-e89b-42d3-8456-426614174000"], + "title": "Archive Id" + }, + "description": "The ID of the archive in the format 'archive-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArchiveUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Archive" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["archives"], + "summary": "Delete Archive", + "description": "Delete an archive by its ID.", + "operationId": "delete_archive", + "parameters": [ + { + "name": "archive_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^archive-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the archive in the format 'archive-'", + "examples": ["archive-123e4567-e89b-42d3-8456-426614174000"], + "title": "Archive Id" + }, + "description": "The ID of the archive in the format 'archive-'" + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/archives/{archive_id}/agents": { + "get": { + "tags": ["archives"], + "summary": "List Agents For Archive", + "description": "Get a list of agents that have access to an archive with pagination support.", + "operationId": "list_agents_for_archive", + "parameters": [ + { + "name": "archive_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^archive-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the archive in the format 'archive-'", + "examples": ["archive-123e4567-e89b-42d3-8456-426614174000"], + "title": "Archive Id" + }, + "description": "The ID of the archive in the format 'archive-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order", + "title": "Before" + }, + "description": "Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order", + "title": "After" + }, + "description": "Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of agents to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of agents to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "enum": [ + "agent.blocks", + "agent.identities", + "agent.managed_group", + "agent.pending_approval", + "agent.secrets", + "agent.sources", + "agent.tags", + "agent.tools" + ], + "type": "string" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default.", + "default": [], + "title": "Include" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentState" + }, + "title": "Response List Agents For Archive" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/archives/{archive_id}/passages": { + "post": { + "tags": ["archives"], + "summary": "Create Passage In Archive", + "description": "Create a new passage in an archive.\n\nThis adds a passage to the archive and creates embeddings for vector storage.", + "operationId": "create_passage_in_archive", + "parameters": [ + { + "name": "archive_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^archive-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the archive in the format 'archive-'", + "examples": ["archive-123e4567-e89b-42d3-8456-426614174000"], + "title": "Archive Id" + }, + "description": "The ID of the archive in the format 'archive-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassageCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Passage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/archives/{archive_id}/passages/batch": { + "post": { + "tags": ["archives"], + "summary": "Create Passages In Archive", + "description": "Create multiple passages in an archive.\n\nThis adds passages to the archive and creates embeddings for vector storage.", + "operationId": "create_passages_in_archive", + "parameters": [ + { + "name": "archive_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^archive-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the archive in the format 'archive-'", + "examples": ["archive-123e4567-e89b-42d3-8456-426614174000"], + "title": "Archive Id" + }, + "description": "The ID of the archive in the format 'archive-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassageBatchCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Passage" + }, + "title": "Response Create Passages In Archive" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/archives/{archive_id}/passages/{passage_id}": { + "delete": { + "tags": ["archives"], + "summary": "Delete Passage From Archive", + "description": "Delete a passage from an archive.\n\nThis permanently removes the passage from both the database and vector storage (if applicable).", + "operationId": "delete_passage_from_archive", + "parameters": [ + { + "name": "archive_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^archive-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the archive in the format 'archive-'", + "examples": ["archive-123e4567-e89b-42d3-8456-426614174000"], + "title": "Archive Id" + }, + "description": "The ID of the archive in the format 'archive-'" + }, + { + "name": "passage_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^passage-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the passage in the format 'passage-'", + "examples": ["passage-123e4567-e89b-42d3-8456-426614174000"], + "title": "Passage Id" + }, + "description": "The ID of the passage in the format 'passage-'" + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/{tool_id}": { + "delete": { + "tags": ["tools"], + "summary": "Delete Tool", + "description": "Delete a tool by name", + "operationId": "delete_tool", + "parameters": [ + { + "name": "tool_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the tool in the format 'tool-'", + "examples": ["tool-123e4567-e89b-42d3-8456-426614174000"], + "title": "Tool Id" + }, + "description": "The ID of the tool in the format 'tool-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["tools"], + "summary": "Retrieve Tool", + "description": "Get a tool by ID", + "operationId": "retrieve_tool", + "parameters": [ + { + "name": "tool_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the tool in the format 'tool-'", + "examples": ["tool-123e4567-e89b-42d3-8456-426614174000"], + "title": "Tool Id" + }, + "description": "The ID of the tool in the format 'tool-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tool" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["tools"], + "summary": "Modify Tool", + "description": "Update an existing tool", + "operationId": "modify_tool", + "parameters": [ + { + "name": "tool_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the tool in the format 'tool-'", + "examples": ["tool-123e4567-e89b-42d3-8456-426614174000"], + "title": "Tool Id" + }, + "description": "The ID of the tool in the format 'tool-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tool" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/count": { + "get": { + "tags": ["tools"], + "summary": "Count Tools", + "description": "Get a count of all tools available to agents belonging to the org of the user.", + "operationId": "count_tools", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + } + }, + { + "name": "names", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter by specific tool names", + "title": "Names" + }, + "description": "Filter by specific tool names" + }, + { + "name": "tool_ids", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter by specific tool IDs - accepts repeated params or comma-separated values", + "title": "Tool Ids" + }, + "description": "Filter by specific tool IDs - accepts repeated params or comma-separated values" + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search tool names (case-insensitive partial match)", + "title": "Search" + }, + "description": "Search tool names (case-insensitive partial match)" + }, + { + "name": "tool_types", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter by tool type(s) - accepts repeated params or comma-separated values", + "title": "Tool Types" + }, + "description": "Filter by tool type(s) - accepts repeated params or comma-separated values" + }, + { + "name": "exclude_tool_types", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Tool type(s) to exclude - accepts repeated params or comma-separated values", + "title": "Exclude Tool Types" + }, + "description": "Tool type(s) to exclude - accepts repeated params or comma-separated values" + }, + { + "name": "return_only_letta_tools", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Count only tools with tool_type starting with 'letta_'", + "default": false, + "title": "Return Only Letta Tools" + }, + "description": "Count only tools with tool_type starting with 'letta_'" + }, + { + "name": "exclude_letta_tools", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Exclude built-in Letta tools from the count", + "default": false, + "title": "Exclude Letta Tools" + }, + "description": "Exclude built-in Letta tools from the count" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "integer", + "title": "Response Count Tools" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/": { + "get": { + "tags": ["tools"], + "summary": "List Tools", + "description": "Get a list of all tools available to agents.", + "operationId": "list_tools", + "parameters": [ + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Tool ID cursor for pagination. Returns tools that come before this tool ID in the specified sort order", + "title": "Before" + }, + "description": "Tool ID cursor for pagination. Returns tools that come before this tool ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Tool ID cursor for pagination. Returns tools that come after this tool ID in the specified sort order", + "title": "After" + }, + "description": "Tool ID cursor for pagination. Returns tools that come after this tool ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of tools to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of tools to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for tools by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for tools by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter by single tool name", + "title": "Name" + }, + "description": "Filter by single tool name" + }, + { + "name": "names", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter by specific tool names", + "title": "Names" + }, + "description": "Filter by specific tool names" + }, + { + "name": "tool_ids", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter by specific tool IDs - accepts repeated params or comma-separated values", + "title": "Tool Ids" + }, + "description": "Filter by specific tool IDs - accepts repeated params or comma-separated values" + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search tool names (case-insensitive partial match)", + "title": "Search" + }, + "description": "Search tool names (case-insensitive partial match)" + }, + { + "name": "tool_types", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter by tool type(s) - accepts repeated params or comma-separated values", + "title": "Tool Types" + }, + "description": "Filter by tool type(s) - accepts repeated params or comma-separated values" + }, + { + "name": "exclude_tool_types", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Tool type(s) to exclude - accepts repeated params or comma-separated values", + "title": "Exclude Tool Types" + }, + "description": "Tool type(s) to exclude - accepts repeated params or comma-separated values" + }, + { + "name": "return_only_letta_tools", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Return only tools with tool_type starting with 'letta_'", + "default": false, + "title": "Return Only Letta Tools" + }, + "description": "Return only tools with tool_type starting with 'letta_'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tool" + }, + "title": "Response List Tools" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["tools"], + "summary": "Create Tool", + "description": "Create a new tool", + "operationId": "create_tool", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tool" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "put": { + "tags": ["tools"], + "summary": "Upsert Tool", + "description": "Create or update a tool", + "operationId": "upsert_tool", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tool" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/search": { + "post": { + "tags": ["tools"], + "summary": "Search Tools", + "description": "Search tools using semantic search.\n\nRequires tool embedding to be enabled (embed_tools=True). Uses vector search,\nfull-text search, or hybrid mode to find tools matching the query.\n\nReturns tools ranked by relevance with their search scores.", + "operationId": "search_tools", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolSearchRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ToolSearchResult" + }, + "title": "Response Search Tools" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/add-base-tools": { + "post": { + "tags": ["tools"], + "summary": "Upsert Base Tools", + "description": "Upsert base tools", + "operationId": "add_base_tools", + "parameters": [], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tool" + }, + "title": "Response Add Base Tools" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/run": { + "post": { + "tags": ["tools"], + "summary": "Run Tool From Source", + "description": "Attempt to build a tool from source, then run it on the provided arguments", + "operationId": "run_tool_from_source", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolRunFromSource" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolReturnMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/mcp/servers": { + "get": { + "tags": ["tools"], + "summary": "List Mcp Servers", + "description": "Get a list of all configured MCP servers", + "operationId": "list_mcp_servers", + "parameters": [], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/components/schemas/SSEServerConfig" + }, + { + "$ref": "#/components/schemas/StdioServerConfig" + }, + { + "$ref": "#/components/schemas/StreamableHTTPServerConfig" + } + ] + }, + "title": "Response List Mcp Servers" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "put": { + "tags": ["tools"], + "summary": "Add Mcp Server To Config", + "description": "Add a new MCP server to the Letta MCP server config", + "operationId": "add_mcp_server", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StdioServerConfig" + }, + { + "$ref": "#/components/schemas/SSEServerConfig" + }, + { + "$ref": "#/components/schemas/StreamableHTTPServerConfig" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/StdioServerConfig" + }, + { + "$ref": "#/components/schemas/SSEServerConfig" + }, + { + "$ref": "#/components/schemas/StreamableHTTPServerConfig" + } + ] + }, + "title": "Response Add Mcp Server" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/mcp/servers/{mcp_server_name}/tools": { + "get": { + "tags": ["tools"], + "summary": "List Mcp Tools By Server", + "description": "Get a list of all tools for a specific MCP server", + "operationId": "list_mcp_tools_by_server", + "parameters": [ + { + "name": "mcp_server_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MCPTool" + }, + "title": "Response List Mcp Tools By Server" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/mcp/servers/{mcp_server_name}/resync": { + "post": { + "tags": ["tools"], + "summary": "Resync Mcp Server Tools", + "description": "Resync tools for an MCP server by:\n1. Fetching current tools from the MCP server\n2. Deleting tools that no longer exist on the server\n3. Updating schemas for existing tools\n4. Adding new tools from the server\n\nReturns a summary of changes made.", + "operationId": "resync_mcp_server_tools", + "parameters": [ + { + "name": "mcp_server_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Name" + } + }, + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/mcp/servers/{mcp_server_name}/{mcp_tool_name}": { + "post": { + "tags": ["tools"], + "summary": "Add Mcp Tool", + "description": "Register a new MCP tool as a Letta server by MCP server + tool name", + "operationId": "add_mcp_tool", + "parameters": [ + { + "name": "mcp_server_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Name" + } + }, + { + "name": "mcp_tool_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Tool Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tool" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/mcp/servers/{mcp_server_name}": { + "patch": { + "tags": ["tools"], + "summary": "Update Mcp Server", + "description": "Update an existing MCP server configuration", + "operationId": "update_mcp_server", + "parameters": [ + { + "name": "mcp_server_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Name" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/letta__schemas__mcp__UpdateStdioMCPServer" + }, + { + "$ref": "#/components/schemas/letta__schemas__mcp__UpdateSSEMCPServer" + }, + { + "$ref": "#/components/schemas/letta__schemas__mcp__UpdateStreamableHTTPMCPServer" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StdioServerConfig" + }, + { + "$ref": "#/components/schemas/SSEServerConfig" + }, + { + "$ref": "#/components/schemas/StreamableHTTPServerConfig" + } + ], + "title": "Response Update Mcp Server" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["tools"], + "summary": "Delete Mcp Server From Config", + "description": "Delete a MCP server configuration", + "operationId": "delete_mcp_server", + "parameters": [ + { + "name": "mcp_server_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/StdioServerConfig" + }, + { + "$ref": "#/components/schemas/SSEServerConfig" + }, + { + "$ref": "#/components/schemas/StreamableHTTPServerConfig" + } + ] + }, + "title": "Response Delete Mcp Server" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/mcp/servers/test": { + "post": { + "tags": ["tools"], + "summary": "Test Mcp Server", + "description": "Test connection to an MCP server without adding it.\nReturns the list of available tools if successful.", + "operationId": "test_mcp_server", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StdioServerConfig" + }, + { + "$ref": "#/components/schemas/SSEServerConfig" + }, + { + "$ref": "#/components/schemas/StreamableHTTPServerConfig" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/mcp/servers/connect": { + "post": { + "tags": ["tools"], + "summary": "Connect Mcp Server", + "description": "Connect to an MCP server with support for OAuth via SSE.\nReturns a stream of events handling authorization state and exchange if OAuth is required.", + "operationId": "connect_mcp_server", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StdioServerConfig" + }, + { + "$ref": "#/components/schemas/SSEServerConfig" + }, + { + "$ref": "#/components/schemas/StreamableHTTPServerConfig" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": {} + }, + "text/event-stream": { + "description": "Server-Sent Events stream" + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/mcp/servers/{mcp_server_name}/tools/{tool_name}/execute": { + "post": { + "tags": ["tools"], + "summary": "Execute Mcp Tool", + "description": "Execute a specific MCP tool from a configured server.\nReturns the tool execution result.", + "operationId": "execute_mcp_tool", + "parameters": [ + { + "name": "mcp_server_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Name" + } + }, + { + "name": "tool_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Tool Name" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/letta__server__rest_api__routers__v1__tools__ToolExecuteRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tools/mcp/oauth/callback": { + "get": { + "tags": ["tools"], + "summary": "Mcp Oauth Callback", + "description": "Handle OAuth callback for MCP server authentication.\nSession is identified via the state parameter instead of URL path.", + "operationId": "mcp_oauth_callback", + "parameters": [ + { + "name": "code", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "OAuth authorization code", + "title": "Code" + }, + "description": "OAuth authorization code" + }, + { + "name": "state", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "OAuth state parameter", + "title": "State" + }, + "description": "OAuth state parameter" + }, + { + "name": "error", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "OAuth error", + "title": "Error" + }, + "description": "OAuth error" + }, + { + "name": "error_description", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "OAuth error description", + "title": "Error Description" + }, + "description": "OAuth error description" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/sources/count": { + "get": { + "tags": ["sources"], + "summary": "Count Sources", + "description": "Count all data sources created by a user.", + "operationId": "count_sources", + "deprecated": true, + "parameters": [], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "integer", + "title": "Response Count Sources" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/sources/{source_id}": { + "get": { + "tags": ["sources"], + "summary": "Retrieve Source", + "description": "Get all sources", + "operationId": "retrieve_source", + "deprecated": true, + "parameters": [ + { + "name": "source_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Source Id" + }, + "description": "The ID of the source in the format 'source-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Source" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["sources"], + "summary": "Modify Source", + "description": "Update the name or documentation of an existing data source.", + "operationId": "modify_source", + "deprecated": true, + "parameters": [ + { + "name": "source_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Source Id" + }, + "description": "The ID of the source in the format 'source-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SourceUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Source" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["sources"], + "summary": "Delete Source", + "description": "Delete a data source.", + "operationId": "delete_source", + "deprecated": true, + "parameters": [ + { + "name": "source_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Source Id" + }, + "description": "The ID of the source in the format 'source-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/sources/name/{source_name}": { + "get": { + "tags": ["sources"], + "summary": "Get Source Id By Name", + "description": "Get a source by name", + "operationId": "get_source_id_by_name", + "deprecated": true, + "parameters": [ + { + "name": "source_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Source Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string", + "title": "Response Get Source Id By Name" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/sources/metadata": { + "get": { + "tags": ["sources"], + "summary": "Get Sources Metadata", + "description": "Get aggregated metadata for all sources in an organization.\n\nReturns structured metadata including:\n- Total number of sources\n- Total number of files across all sources\n- Total size of all files\n- Per-source breakdown with file details (file_name, file_size per file) if include_detailed_per_source_metadata is True", + "operationId": "get_sources_metadata", + "deprecated": true, + "parameters": [ + { + "name": "include_detailed_per_source_metadata", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Include Detailed Per Source Metadata" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationSourcesStats" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/sources/": { + "get": { + "tags": ["sources"], + "summary": "List Sources", + "description": "List all data sources created by a user.", + "operationId": "list_sources", + "deprecated": true, + "parameters": [], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Source" + }, + "title": "Response List Sources" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["sources"], + "summary": "Create Source", + "description": "Create a new data source.", + "operationId": "create_source", + "deprecated": true, + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SourceCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Source" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/sources/{source_id}/upload": { + "post": { + "tags": ["sources"], + "summary": "Upload File To Source", + "description": "Upload a file to a data source.", + "operationId": "upload_file_to_source", + "deprecated": true, + "parameters": [ + { + "name": "source_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Source Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "duplicate_handling", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/DuplicateFileHandling", + "description": "How to handle duplicate filenames", + "default": "suffix" + }, + "description": "How to handle duplicate filenames" + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional custom name to override the uploaded file's name", + "title": "Name" + }, + "description": "Optional custom name to override the uploaded file's name" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_upload_file_to_source" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileMetadata" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/sources/{source_id}/agents": { + "get": { + "tags": ["sources"], + "summary": "Get Agents For Source", + "description": "Get all agent IDs that have the specified source attached.", + "operationId": "get_agents_for_source", + "deprecated": true, + "parameters": [ + { + "name": "source_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Source Id" + }, + "description": "The ID of the source in the format 'source-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Response Get Agents For Source" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/sources/{source_id}/passages": { + "get": { + "tags": ["sources"], + "summary": "List Source Passages", + "description": "List all passages associated with a data source.", + "operationId": "list_source_passages", + "deprecated": true, + "parameters": [ + { + "name": "source_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Source Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message after which to retrieve the returned messages.", + "title": "After" + }, + "description": "Message after which to retrieve the returned messages." + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message before which to retrieve the returned messages.", + "title": "Before" + }, + "description": "Message before which to retrieve the returned messages." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "description": "Maximum number of messages to retrieve.", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of messages to retrieve." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Passage" + }, + "title": "Response List Source Passages" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/sources/{source_id}/files": { + "get": { + "tags": ["sources"], + "summary": "List Source Files", + "description": "List paginated files associated with a data source.", + "operationId": "list_source_files", + "deprecated": true, + "parameters": [ + { + "name": "source_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Source Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "description": "Number of files to return", + "default": 1000, + "title": "Limit" + }, + "description": "Number of files to return" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Pagination cursor to fetch the next set of results", + "title": "After" + }, + "description": "Pagination cursor to fetch the next set of results" + }, + { + "name": "include_content", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to include full file content", + "default": false, + "title": "Include Content" + }, + "description": "Whether to include full file content" + }, + { + "name": "check_status_updates", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to check and update file processing status (from the vector db service). If False, will not fetch and update the status, which may lead to performance gains.", + "default": true, + "title": "Check Status Updates" + }, + "description": "Whether to check and update file processing status (from the vector db service). If False, will not fetch and update the status, which may lead to performance gains." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileMetadata" + }, + "title": "Response List Source Files" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/sources/{source_id}/files/{file_id}": { + "get": { + "tags": ["sources"], + "summary": "Get File Metadata", + "description": "Retrieve metadata for a specific file by its ID.", + "operationId": "get_file_metadata", + "deprecated": true, + "parameters": [ + { + "name": "source_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Source Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the file in the format 'file-'", + "examples": ["file-123e4567-e89b-42d3-8456-426614174000"], + "title": "File Id" + }, + "description": "The ID of the file in the format 'file-'" + }, + { + "name": "include_content", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to include full file content", + "default": false, + "title": "Include Content" + }, + "description": "Whether to include full file content" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileMetadata" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/sources/{source_id}/{file_id}": { + "delete": { + "tags": ["sources"], + "summary": "Delete File From Source", + "description": "Delete a data source.", + "operationId": "delete_file_from_source", + "deprecated": true, + "parameters": [ + { + "name": "source_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Source Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the file in the format 'file-'", + "examples": ["file-123e4567-e89b-42d3-8456-426614174000"], + "title": "File Id" + }, + "description": "The ID of the file in the format 'file-'" + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/folders/count": { + "get": { + "tags": ["folders"], + "summary": "Count Folders", + "description": "Count all data folders created by a user.", + "operationId": "count_folders", + "parameters": [], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "integer", + "title": "Response Count Folders" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/folders/{folder_id}": { + "get": { + "tags": ["folders"], + "summary": "Retrieve Folder", + "description": "Get a folder by ID", + "operationId": "retrieve_folder", + "parameters": [ + { + "name": "folder_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Folder Id" + }, + "description": "The ID of the source in the format 'source-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Folder" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["folders"], + "summary": "Modify Folder", + "description": "Update the name or documentation of an existing data folder.", + "operationId": "modify_folder", + "parameters": [ + { + "name": "folder_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Folder Id" + }, + "description": "The ID of the source in the format 'source-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SourceUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Folder" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["folders"], + "summary": "Delete Folder", + "description": "Delete a data folder.", + "operationId": "delete_folder", + "parameters": [ + { + "name": "folder_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Folder Id" + }, + "description": "The ID of the source in the format 'source-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/folders/name/{folder_name}": { + "get": { + "tags": ["folders"], + "summary": "Get Folder By Name", + "description": "**Deprecated**: Please use the list endpoint `GET /v1/folders?name=` instead.\n\n\nGet a folder by name.", + "operationId": "get_folder_by_name", + "deprecated": true, + "parameters": [ + { + "name": "folder_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Folder Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string", + "title": "Response Get Folder By Name" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/folders/metadata": { + "get": { + "tags": ["folders"], + "summary": "Retrieve Metadata", + "description": "Get aggregated metadata for all folders in an organization.\n\nReturns structured metadata including:\n- Total number of folders\n- Total number of files across all folders\n- Total size of all files\n- Per-source breakdown with file details (file_name, file_size per file) if include_detailed_per_source_metadata is True", + "operationId": "retrieve_metadata", + "parameters": [ + { + "name": "include_detailed_per_source_metadata", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Include Detailed Per Source Metadata" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganizationSourcesStats" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/folders/": { + "get": { + "tags": ["folders"], + "summary": "List Folders", + "description": "List all data folders created by a user.", + "operationId": "list_folders", + "parameters": [ + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Folder ID cursor for pagination. Returns folders that come before this folder ID in the specified sort order", + "title": "Before" + }, + "description": "Folder ID cursor for pagination. Returns folders that come before this folder ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Folder ID cursor for pagination. Returns folders that come after this folder ID in the specified sort order", + "title": "After" + }, + "description": "Folder ID cursor for pagination. Returns folders that come after this folder ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of folders to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of folders to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for folders by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "asc", + "title": "Order" + }, + "description": "Sort order for folders by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Folder name to filter by", + "title": "Name" + }, + "description": "Folder name to filter by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Folder" + }, + "title": "Response List Folders" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["folders"], + "summary": "Create Folder", + "description": "Create a new data folder.", + "operationId": "create_folder", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SourceCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Folder" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/folders/{folder_id}/upload": { + "post": { + "tags": ["folders"], + "summary": "Upload File To Folder", + "description": "Upload a file to a data folder.", + "operationId": "upload_file_to_folder", + "parameters": [ + { + "name": "folder_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Folder Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "duplicate_handling", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/DuplicateFileHandling", + "description": "How to handle duplicate filenames", + "default": "suffix" + }, + "description": "How to handle duplicate filenames" + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional custom name to override the uploaded file's name", + "title": "Name" + }, + "description": "Optional custom name to override the uploaded file's name" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_upload_file_to_folder" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileMetadata" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/folders/{folder_id}/agents": { + "get": { + "tags": ["folders"], + "summary": "List Agents For Folder", + "description": "Get all agent IDs that have the specified folder attached.", + "operationId": "list_agents_for_folder", + "parameters": [ + { + "name": "folder_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Folder Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order", + "title": "Before" + }, + "description": "Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order", + "title": "After" + }, + "description": "Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of agents to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of agents to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Response List Agents For Folder" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/folders/{folder_id}/passages": { + "get": { + "tags": ["folders"], + "summary": "List Folder Passages", + "description": "List all passages associated with a data folder.", + "operationId": "list_folder_passages", + "parameters": [ + { + "name": "folder_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Folder Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Passage ID cursor for pagination. Returns passages that come before this passage ID in the specified sort order", + "title": "Before" + }, + "description": "Passage ID cursor for pagination. Returns passages that come before this passage ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Passage ID cursor for pagination. Returns passages that come after this passage ID in the specified sort order", + "title": "After" + }, + "description": "Passage ID cursor for pagination. Returns passages that come after this passage ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of passages to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of passages to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for passages by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for passages by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Passage" + }, + "title": "Response List Folder Passages" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/folders/{folder_id}/files": { + "get": { + "tags": ["folders"], + "summary": "List Files For Folder", + "description": "List paginated files associated with a data folder.", + "operationId": "list_files_for_folder", + "parameters": [ + { + "name": "folder_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Folder Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "File ID cursor for pagination. Returns files that come before this file ID in the specified sort order", + "title": "Before" + }, + "description": "File ID cursor for pagination. Returns files that come before this file ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "File ID cursor for pagination. Returns files that come after this file ID in the specified sort order", + "title": "After" + }, + "description": "File ID cursor for pagination. Returns files that come after this file ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of files to return", + "default": 1000, + "title": "Limit" + }, + "description": "Maximum number of files to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for files by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for files by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "include_content", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to include full file content", + "default": false, + "title": "Include Content" + }, + "description": "Whether to include full file content" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileMetadata" + }, + "title": "Response List Files For Folder" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/folders/{folder_id}/files/{file_id}": { + "get": { + "tags": ["folders"], + "summary": "Retrieve File", + "description": "Retrieve a file from a folder by ID.", + "operationId": "retrieve_file", + "parameters": [ + { + "name": "folder_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Folder Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the file in the format 'file-'", + "examples": ["file-123e4567-e89b-42d3-8456-426614174000"], + "title": "File Id" + }, + "description": "The ID of the file in the format 'file-'" + }, + { + "name": "include_content", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to include full file content", + "default": false, + "title": "Include Content" + }, + "description": "Whether to include full file content" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileMetadata" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/folders/{folder_id}/{file_id}": { + "delete": { + "tags": ["folders"], + "summary": "Delete File From Folder", + "description": "Delete a file from a folder.", + "operationId": "delete_file_from_folder", + "parameters": [ + { + "name": "folder_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Folder Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the file in the format 'file-'", + "examples": ["file-123e4567-e89b-42d3-8456-426614174000"], + "title": "File Id" + }, + "description": "The ID of the file in the format 'file-'" + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/": { + "get": { + "tags": ["agents"], + "summary": "List Agents", + "description": "Get a list of all agents.", + "operationId": "list_agents", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Name of the agent", + "title": "Name" + }, + "description": "Name of the agent" + }, + { + "name": "tags", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "List of tags to filter agents by", + "title": "Tags" + }, + "description": "List of tags to filter agents by" + }, + { + "name": "match_all_tags", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, only returns agents that match ALL given tags. Otherwise, return agents that have ANY of the passed-in tags.", + "default": false, + "title": "Match All Tags" + }, + "description": "If True, only returns agents that match ALL given tags. Otherwise, return agents that have ANY of the passed-in tags." + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Cursor for pagination", + "title": "Before" + }, + "description": "Cursor for pagination" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Cursor for pagination", + "title": "After" + }, + "description": "Cursor for pagination" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Limit for pagination", + "default": 50, + "title": "Limit" + }, + "description": "Limit for pagination" + }, + { + "name": "query_text", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search agents by name", + "title": "Query Text" + }, + "description": "Search agents by name" + }, + { + "name": "project_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search agents by project ID - this will default to your default project on cloud", + "title": "Project Id" + }, + "description": "Search agents by project ID - this will default to your default project on cloud" + }, + { + "name": "template_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search agents by template ID", + "title": "Template Id" + }, + "description": "Search agents by template ID" + }, + { + "name": "base_template_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search agents by base template ID", + "title": "Base Template Id" + }, + "description": "Search agents by base template ID" + }, + { + "name": "identity_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search agents by identity ID", + "title": "Identity Id" + }, + "description": "Search agents by identity ID" + }, + { + "name": "identifier_keys", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Search agents by identifier keys", + "title": "Identifier Keys" + }, + "description": "Search agents by identifier keys" + }, + { + "name": "include_relationships", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. If not provided, all relationships are loaded by default. Using this can optimize performance by reducing unnecessary joins.This is a legacy parameter, and no longer supported after 1.0.0 SDK versions.", + "deprecated": true, + "title": "Include Relationships" + }, + "description": "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. If not provided, all relationships are loaded by default. Using this can optimize performance by reducing unnecessary joins.This is a legacy parameter, and no longer supported after 1.0.0 SDK versions.", + "deprecated": true + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "enum": [ + "agent.blocks", + "agent.identities", + "agent.managed_group", + "agent.pending_approval", + "agent.secrets", + "agent.sources", + "agent.tags", + "agent.tools" + ], + "type": "string" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default.", + "default": [], + "title": "Include" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "enum": ["created_at", "updated_at", "last_run_completion"], + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "ascending", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to sort agents oldest to newest (True) or newest to oldest (False, default)", + "deprecated": true, + "default": false, + "title": "Ascending" + }, + "description": "Whether to sort agents oldest to newest (True) or newest to oldest (False, default)", + "deprecated": true + }, + { + "name": "sort_by", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Field to sort by. Options: 'created_at' (default), 'last_run_completion'", + "deprecated": true, + "default": "created_at", + "title": "Sort By" + }, + "description": "Field to sort by. Options: 'created_at' (default), 'last_run_completion'", + "deprecated": true + }, + { + "name": "last_stop_reason", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StopReasonType" + }, + { + "type": "null" + } + ], + "description": "Filter agents by their last stop reason.", + "title": "Last Stop Reason" + }, + "description": "Filter agents by their last stop reason." + }, + { + "name": "created_by_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter agents by the user who created them.", + "title": "Created By Id" + }, + "description": "Filter agents by the user who created them." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentState" + }, + "title": "Response List Agents" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["agents"], + "summary": "Create Agent", + "description": "Create an agent.", + "operationId": "create_agent", + "parameters": [ + { + "name": "X-Project", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The project slug to associate with the agent (cloud only).", + "title": "X-Project" + }, + "description": "The project slug to associate with the agent (cloud only)." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAgentRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentState" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/count": { + "get": { + "tags": ["agents"], + "summary": "Count Agents", + "description": "Get the total number of agents with optional filtering.\nSupports the same filters as list_agents for consistent querying.", + "operationId": "count_agents", + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Name of the agent", + "title": "Name" + }, + "description": "Name of the agent" + }, + { + "name": "tags", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "List of tags to filter agents by", + "title": "Tags" + }, + "description": "List of tags to filter agents by" + }, + { + "name": "match_all_tags", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, only counts agents that match ALL given tags. Otherwise, counts agents that have ANY of the passed-in tags.", + "default": false, + "title": "Match All Tags" + }, + "description": "If True, only counts agents that match ALL given tags. Otherwise, counts agents that have ANY of the passed-in tags." + }, + { + "name": "query_text", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search agents by name", + "title": "Query Text" + }, + "description": "Search agents by name" + }, + { + "name": "project_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search agents by project ID - this will default to your default project on cloud", + "title": "Project Id" + }, + "description": "Search agents by project ID - this will default to your default project on cloud" + }, + { + "name": "template_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search agents by template ID", + "title": "Template Id" + }, + "description": "Search agents by template ID" + }, + { + "name": "base_template_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search agents by base template ID", + "title": "Base Template Id" + }, + "description": "Search agents by base template ID" + }, + { + "name": "identity_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search agents by identity ID", + "title": "Identity Id" + }, + "description": "Search agents by identity ID" + }, + { + "name": "identifier_keys", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Search agents by identifier keys", + "title": "Identifier Keys" + }, + "description": "Search agents by identifier keys" + }, + { + "name": "last_stop_reason", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StopReasonType" + }, + { + "type": "null" + } + ], + "description": "Filter agents by their last stop reason.", + "title": "Last Stop Reason" + }, + "description": "Filter agents by their last stop reason." + }, + { + "name": "created_by_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter agents by the user who created them.", + "title": "Created By Id" + }, + "description": "Filter agents by the user who created them." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "integer", + "title": "Response Count Agents" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/export": { + "get": { + "tags": ["agents"], + "summary": "Export Agent", + "description": "Export the serialized JSON representation of an agent, formatted with indentation.", + "operationId": "export_agent", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Agent Id" + } + }, + { + "name": "max_steps", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "deprecated": true, + "default": 100, + "title": "Max Steps" + }, + "deprecated": true + }, + { + "name": "use_legacy_format", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, exports using the legacy single-agent 'v1' format with inline tools/blocks. If False, exports using the new multi-entity 'v2' format, with separate agents, tools, blocks, files, etc.", + "deprecated": true, + "default": false, + "title": "Use Legacy Format" + }, + "description": "If True, exports using the legacy single-agent 'v1' format with inline tools/blocks. If False, exports using the new multi-entity 'v2' format, with separate agents, tools, blocks, files, etc.", + "deprecated": true + }, + { + "name": "conversation_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Conversation ID to export. If provided, uses messages from this conversation instead of the agent's global message history.", + "title": "Conversation Id" + }, + "description": "Conversation ID to export. If provided, uses messages from this conversation instead of the agent's global message history." + }, + { + "name": "scrub_messages", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, excludes all messages from the export. Useful for sharing agent configs without conversation history.", + "default": false, + "title": "Scrub Messages" + }, + "description": "If True, excludes all messages from the export. Useful for sharing agent configs without conversation history." + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_export_agent" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["agents"], + "summary": "Export Agent With Skills", + "description": "Export the serialized JSON representation of an agent with optional skills.\n\nThis POST endpoint allows including skills in the export by providing them in the request body.\nSkills are resolved client-side and passed as SkillSchema objects containing the skill files.", + "operationId": "export_agent_with_skills", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Agent Id" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExportAgentRequest" + }, + { + "type": "null" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/import": { + "post": { + "tags": ["agents"], + "summary": "Import Agent", + "description": "Import a serialized agent file and recreate the agent(s) in the system.\nReturns the IDs of all imported agents.", + "operationId": "import_agent", + "parameters": [ + { + "name": "x-override-embedding-model", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "X-Override-Embedding-Model" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_import_agent" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportedAgentsResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/context": { + "get": { + "tags": ["agents"], + "summary": "Retrieve Agent Context Window", + "description": "Retrieve the context window of a specific agent.", + "operationId": "retrieve_agent_context_window", + "deprecated": true, + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "conversation_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Conversation ID to get context window for. If provided, uses messages from this conversation.", + "title": "Conversation Id" + }, + "description": "Conversation ID to get context window for. If provided, uses messages from this conversation." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextWindowOverview" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}": { + "patch": { + "tags": ["agents"], + "summary": "Modify Agent", + "description": "Update an existing agent.", + "operationId": "modify_agent", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAgent" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentState" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["agents"], + "summary": "Retrieve Agent", + "description": "Get the state of the agent.", + "operationId": "retrieve_agent", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "include_relationships", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. If not provided, all relationships are loaded by default. Using this can optimize performance by reducing unnecessary joins.This is a legacy parameter, and no longer supported after 1.0.0 SDK versions.", + "deprecated": true, + "title": "Include Relationships" + }, + "description": "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. If not provided, all relationships are loaded by default. Using this can optimize performance by reducing unnecessary joins.This is a legacy parameter, and no longer supported after 1.0.0 SDK versions.", + "deprecated": true + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "enum": [ + "agent.blocks", + "agent.identities", + "agent.managed_group", + "agent.pending_approval", + "agent.secrets", + "agent.sources", + "agent.tags", + "agent.tools" + ], + "type": "string" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default.", + "default": [], + "title": "Include" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentState" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["agents"], + "summary": "Delete Agent", + "description": "Delete an agent.", + "operationId": "delete_agent", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/tools": { + "get": { + "tags": ["agents"], + "summary": "List Tools For Agent", + "description": "Get tools from an existing agent.", + "operationId": "list_tools_for_agent", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Tool ID cursor for pagination. Returns tools that come before this tool ID in the specified sort order", + "title": "Before" + }, + "description": "Tool ID cursor for pagination. Returns tools that come before this tool ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Tool ID cursor for pagination. Returns tools that come after this tool ID in the specified sort order", + "title": "After" + }, + "description": "Tool ID cursor for pagination. Returns tools that come after this tool ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of tools to return", + "default": 10, + "title": "Limit" + }, + "description": "Maximum number of tools to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for tools by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for tools by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tool" + }, + "title": "Response List Tools For Agent" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/tools/attach/{tool_id}": { + "patch": { + "tags": ["agents"], + "summary": "Attach Tool To Agent", + "description": "Attach a tool to an agent.", + "operationId": "attach_tool_to_agent", + "parameters": [ + { + "name": "tool_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the tool in the format 'tool-'", + "examples": ["tool-123e4567-e89b-42d3-8456-426614174000"], + "title": "Tool Id" + }, + "description": "The ID of the tool in the format 'tool-'" + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentState" + }, + { + "type": "null" + } + ], + "title": "Response Attach Tool To Agent" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/tools/detach/{tool_id}": { + "patch": { + "tags": ["agents"], + "summary": "Detach Tool From Agent", + "description": "Detach a tool from an agent.", + "operationId": "detach_tool_from_agent", + "parameters": [ + { + "name": "tool_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the tool in the format 'tool-'", + "examples": ["tool-123e4567-e89b-42d3-8456-426614174000"], + "title": "Tool Id" + }, + "description": "The ID of the tool in the format 'tool-'" + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentState" + }, + { + "type": "null" + } + ], + "title": "Response Detach Tool From Agent" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/tools/approval/{tool_name}": { + "patch": { + "tags": ["agents"], + "summary": "Modify Approval For Tool", + "description": "Modify the approval requirement for a tool attached to an agent.\n\nAccepts requires_approval via request body (preferred) or query parameter (deprecated).", + "operationId": "modify_approval_for_tool", + "parameters": [ + { + "name": "tool_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Tool Name" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "requires_approval", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether the tool requires approval before execution", + "deprecated": true, + "title": "Requires Approval" + }, + "description": "Whether the tool requires approval before execution", + "deprecated": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ModifyApprovalRequest" + }, + { + "type": "null" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentState" + }, + { + "type": "null" + } + ], + "title": "Response Modify Approval For Tool" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/tools/{tool_name}/run": { + "post": { + "tags": ["agents"], + "summary": "Run Tool For Agent", + "description": "Trigger a tool by name on a specific agent, providing the necessary arguments.\n\nThis endpoint executes a tool that is attached to the agent, using the agent's\nstate and environment variables for execution context.", + "operationId": "run_tool_for_agent", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "tool_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Tool Name" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/letta__schemas__mcp_server__ToolExecuteRequest", + "default": { + "args": {} + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolExecutionResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/sources/attach/{source_id}": { + "patch": { + "tags": ["agents"], + "summary": "Attach Source", + "description": "Attach a source to an agent.", + "operationId": "attach_source_to_agent", + "deprecated": true, + "parameters": [ + { + "name": "source_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Source Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentState" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/folders/attach/{folder_id}": { + "patch": { + "tags": ["agents"], + "summary": "Attach Folder To Agent", + "description": "Attach a folder to an agent.", + "operationId": "attach_folder_to_agent", + "parameters": [ + { + "name": "folder_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Folder Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentState" + }, + { + "type": "null" + } + ], + "title": "Response Attach Folder To Agent" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/sources/detach/{source_id}": { + "patch": { + "tags": ["agents"], + "summary": "Detach Source", + "description": "Detach a source from an agent.", + "operationId": "detach_source_from_agent", + "deprecated": true, + "parameters": [ + { + "name": "source_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Source Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentState" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/folders/detach/{folder_id}": { + "patch": { + "tags": ["agents"], + "summary": "Detach Folder From Agent", + "description": "Detach a folder from an agent.", + "operationId": "detach_folder_from_agent", + "parameters": [ + { + "name": "folder_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], + "title": "Folder Id" + }, + "description": "The ID of the source in the format 'source-'" + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentState" + }, + { + "type": "null" + } + ], + "title": "Response Detach Folder From Agent" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/files/close-all": { + "patch": { + "tags": ["agents"], + "summary": "Close All Files For Agent", + "description": "Closes all currently open files for a given agent.\n\nThis endpoint updates the file state for the agent so that no files are marked as open.\nTypically used to reset the working memory view for the agent.", + "operationId": "close_all_files_for_agent", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Response Close All Files For Agent" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/files/{file_id}/open": { + "patch": { + "tags": ["agents"], + "summary": "Open File For Agent", + "description": "Opens a specific file for a given agent.\n\nThis endpoint marks a specific file as open in the agent's file state.\nThe file will be included in the agent's working memory view.\nReturns a list of file names that were closed due to LRU eviction.", + "operationId": "open_file_for_agent", + "parameters": [ + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the file in the format 'file-'", + "examples": ["file-123e4567-e89b-42d3-8456-426614174000"], + "title": "File Id" + }, + "description": "The ID of the file in the format 'file-'" + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Response Open File For Agent" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/files/{file_id}/close": { + "patch": { + "tags": ["agents"], + "summary": "Close File For Agent", + "description": "Closes a specific file for a given agent.\n\nThis endpoint marks a specific file as closed in the agent's file state.\nThe file will be removed from the agent's working memory view.", + "operationId": "close_file_for_agent", + "parameters": [ + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the file in the format 'file-'", + "examples": ["file-123e4567-e89b-42d3-8456-426614174000"], + "title": "File Id" + }, + "description": "The ID of the file in the format 'file-'" + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/sources": { + "get": { + "tags": ["agents"], + "summary": "List Agent Sources", + "description": "Get the sources associated with an agent.", + "operationId": "list_agent_sources", + "deprecated": true, + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Source ID cursor for pagination. Returns sources that come before this source ID in the specified sort order", + "title": "Before" + }, + "description": "Source ID cursor for pagination. Returns sources that come before this source ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Source ID cursor for pagination. Returns sources that come after this source ID in the specified sort order", + "title": "After" + }, + "description": "Source ID cursor for pagination. Returns sources that come after this source ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of sources to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of sources to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for sources by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for sources by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Source" + }, + "title": "Response List Agent Sources" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/folders": { + "get": { + "tags": ["agents"], + "summary": "List Folders For Agent", + "description": "Get the folders associated with an agent.", + "operationId": "list_folders_for_agent", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Source ID cursor for pagination. Returns sources that come before this source ID in the specified sort order", + "title": "Before" + }, + "description": "Source ID cursor for pagination. Returns sources that come before this source ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Source ID cursor for pagination. Returns sources that come after this source ID in the specified sort order", + "title": "After" + }, + "description": "Source ID cursor for pagination. Returns sources that come after this source ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of sources to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of sources to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for sources by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for sources by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Source" + }, + "title": "Response List Folders For Agent" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/files": { + "get": { + "tags": ["agents"], + "summary": "List Files For Agent", + "description": "Get the files attached to an agent with their open/closed status.", + "operationId": "list_files_for_agent", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "File ID cursor for pagination. Returns files that come before this file ID in the specified sort order", + "title": "Before" + }, + "description": "File ID cursor for pagination. Returns files that come before this file ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "File ID cursor for pagination. Returns files that come after this file ID in the specified sort order", + "title": "After" + }, + "description": "File ID cursor for pagination. Returns files that come after this file ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of files to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of files to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for files by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for files by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Pagination cursor from previous response (deprecated, use before/after)", + "deprecated": true, + "title": "Cursor" + }, + "description": "Pagination cursor from previous response (deprecated, use before/after)", + "deprecated": true + }, + { + "name": "is_open", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Filter by open status (true for open files, false for closed files)", + "title": "Is Open" + }, + "description": "Filter by open status (true for open files, false for closed files)" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedAgentFiles" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/core-memory": { + "get": { + "tags": ["agents"], + "summary": "Retrieve Agent Memory", + "description": "Retrieve the memory state of a specific agent.\nThis endpoint fetches the current memory state of the agent identified by the user ID and agent ID.", + "operationId": "retrieve_agent_memory", + "deprecated": true, + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Memory" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/core-memory/blocks/{block_label}": { + "get": { + "tags": ["agents"], + "summary": "Retrieve Block For Agent", + "description": "Retrieve a core memory block from an agent.", + "operationId": "retrieve_core_memory_block", + "parameters": [ + { + "name": "block_label", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Block Label" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["agents"], + "summary": "Modify Block For Agent", + "description": "Updates a core memory block of an agent.", + "operationId": "modify_core_memory_block", + "parameters": [ + { + "name": "block_label", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Block Label" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/core-memory/blocks": { + "get": { + "tags": ["agents"], + "summary": "List Blocks For Agent", + "description": "Retrieve the core memory blocks of a specific agent.", + "operationId": "list_core_memory_blocks", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order", + "title": "Before" + }, + "description": "Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order", + "title": "After" + }, + "description": "Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of blocks to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of blocks to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlockResponse" + }, + "title": "Response List Core Memory Blocks" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/recompile": { + "post": { + "tags": ["agents"], + "summary": "Recompile Agent", + "description": "Manually trigger system prompt recompilation for an agent.", + "operationId": "recompile_agent", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "update_timestamp", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, update the in-context memory last edit timestamp embedded in the system prompt.", + "default": false, + "title": "Update Timestamp" + }, + "description": "If True, update the in-context memory last edit timestamp embedded in the system prompt." + }, + { + "name": "dry_run", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, do not persist changes; still returns the compiled system prompt.", + "default": false, + "title": "Dry Run" + }, + "description": "If True, do not persist changes; still returns the compiled system prompt." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string", + "title": "Response Recompile Agent" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/system-prompt/recompile": { + "post": { + "tags": ["agents"], + "summary": "Recompile Agent System Prompt", + "description": "Deprecated alias for POST /v1/agents/{agent_id}/recompile.", + "operationId": "recompile_agent_system_prompt", + "deprecated": true, + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "update_timestamp", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, update the in-context memory last edit timestamp embedded in the system prompt.", + "default": false, + "title": "Update Timestamp" + }, + "description": "If True, update the in-context memory last edit timestamp embedded in the system prompt." + }, + { + "name": "dry_run", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, do not persist changes; still returns the compiled system prompt.", + "default": false, + "title": "Dry Run" + }, + "description": "If True, do not persist changes; still returns the compiled system prompt." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string", + "title": "Response Recompile Agent System Prompt" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/core-memory/blocks/attach/{block_id}": { + "patch": { + "tags": ["agents"], + "summary": "Attach Block To Agent", + "description": "Attach a core memory block to an agent.", + "operationId": "attach_core_memory_block", + "parameters": [ + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentState" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/core-memory/blocks/detach/{block_id}": { + "patch": { + "tags": ["agents"], + "summary": "Detach Block From Agent", + "description": "Detach a core memory block from an agent.", + "operationId": "detach_core_memory_block", + "parameters": [ + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentState" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/archives/attach/{archive_id}": { + "patch": { + "tags": ["agents"], + "summary": "Attach Archive To Agent", + "description": "Attach an archive to an agent.", + "operationId": "attach_archive_to_agent", + "parameters": [ + { + "name": "archive_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Archive Id" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/archives/detach/{archive_id}": { + "patch": { + "tags": ["agents"], + "summary": "Detach Archive From Agent", + "description": "Detach an archive from an agent.", + "operationId": "detach_archive_from_agent", + "parameters": [ + { + "name": "archive_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Archive Id" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/identities/attach/{identity_id}": { + "patch": { + "tags": ["agents"], + "summary": "Attach Identity To Agent", + "description": "Attach an identity to an agent.", + "operationId": "attach_identity_to_agent", + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Identity Id" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/identities/detach/{identity_id}": { + "patch": { + "tags": ["agents"], + "summary": "Detach Identity From Agent", + "description": "Detach an identity from an agent.", + "operationId": "detach_identity_from_agent", + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Identity Id" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/archival-memory": { + "get": { + "tags": ["agents"], + "summary": "List Passages", + "description": "Retrieve the memories in an agent's archival memory store (paginated query).", + "operationId": "list_passages", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Unique ID of the memory to start the query range at.", + "title": "After" + }, + "description": "Unique ID of the memory to start the query range at." + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Unique ID of the memory to end the query range at.", + "title": "Before" + }, + "description": "Unique ID of the memory to end the query range at." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "How many results to include in the response.", + "default": 100, + "title": "Limit" + }, + "description": "How many results to include in the response." + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search passages by text", + "title": "Search" + }, + "description": "Search passages by text" + }, + { + "name": "ascending", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether to sort passages oldest to newest (True, default) or newest to oldest (False)", + "default": true, + "title": "Ascending" + }, + "description": "Whether to sort passages oldest to newest (True, default) or newest to oldest (False)" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Passage" + }, + "title": "Response List Passages" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["agents"], + "summary": "Create Passage", + "description": "Insert a memory into an agent's archival memory store.", + "operationId": "create_passage", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateArchivalMemory" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Passage" + }, + "title": "Response Create Passage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/archival-memory/search": { + "get": { + "tags": ["agents"], + "summary": "Search Archival Memory", + "description": "Search archival memory using semantic (embedding-based) search with optional temporal filtering.\n\nThis endpoint allows manual triggering of archival memory searches, enabling users to query\nan agent's archival memory store directly via the API. The search uses the same functionality\nas the agent's archival_memory_search tool but is accessible for external API usage.", + "operationId": "search_archival_memory", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "query", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "String to search for using semantic similarity", + "title": "Query" + }, + "description": "String to search for using semantic similarity" + }, + { + "name": "tags", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Optional list of tags to filter search results", + "title": "Tags" + }, + "description": "Optional list of tags to filter search results" + }, + { + "name": "tag_match_mode", + "in": "query", + "required": false, + "schema": { + "enum": ["any", "all"], + "type": "string", + "description": "How to match tags - 'any' to match passages with any of the tags, 'all' to match only passages with all tags", + "default": "any", + "title": "Tag Match Mode" + }, + "description": "How to match tags - 'any' to match passages with any of the tags, 'all' to match only passages with all tags" + }, + { + "name": "top_k", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of results to return. Uses system default if not specified", + "title": "Top K" + }, + "description": "Maximum number of results to return. Uses system default if not specified" + }, + { + "name": "start_datetime", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "description": "Filter results to passages created after this datetime", + "title": "Start Datetime" + }, + "description": "Filter results to passages created after this datetime" + }, + { + "name": "end_datetime", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "description": "Filter results to passages created before this datetime", + "title": "End Datetime" + }, + "description": "Filter results to passages created before this datetime" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArchivalMemorySearchResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/archival-memory/{memory_id}": { + "delete": { + "tags": ["agents"], + "summary": "Delete Passage", + "description": "Delete a memory from an agent's archival memory store.", + "operationId": "delete_passage", + "parameters": [ + { + "name": "memory_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Memory Id" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/messages": { + "get": { + "tags": ["agents"], + "summary": "List Messages", + "description": "Retrieve message history for an agent.", + "operationId": "list_messages", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order", + "title": "Before" + }, + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order", + "title": "After" + }, + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of messages to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of messages to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "group_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Group ID to filter messages by.", + "title": "Group Id" + }, + "description": "Group ID to filter messages by." + }, + { + "name": "conversation_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Conversation ID to filter messages by.", + "title": "Conversation Id" + }, + "description": "Conversation ID to filter messages by." + }, + { + "name": "use_assistant_message", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to use assistant messages", + "deprecated": true, + "default": true, + "title": "Use Assistant Message" + }, + "description": "Whether to use assistant messages", + "deprecated": true + }, + { + "name": "assistant_message_tool_name", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "The name of the designated message tool.", + "deprecated": true, + "default": "send_message", + "title": "Assistant Message Tool Name" + }, + "description": "The name of the designated message tool.", + "deprecated": true + }, + { + "name": "assistant_message_tool_kwarg", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "The name of the message argument.", + "deprecated": true, + "default": "message", + "title": "Assistant Message Tool Kwarg" + }, + "description": "The name of the message argument.", + "deprecated": true + }, + { + "name": "include_err", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether to include error messages and error statuses. For debugging purposes only.", + "title": "Include Err" + }, + "description": "Whether to include error messages and error statuses. For debugging purposes only." + }, + { + "name": "include_return_message_types", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageType" + } + }, + { + "type": "null" + } + ], + "description": "Message types to include in response. When null, all message types are returned.", + "title": "Include Return Message Types" + }, + "description": "Message types to include in response. When null, all message types are returned." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LettaMessageUnion" + }, + "title": "Response List Messages" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["agents"], + "summary": "Send Message", + "description": "Process a user message and return the agent's response.\nThis endpoint accepts a message from a user and processes it through the agent.\n\n**Note:** Sending multiple concurrent requests to the same agent can lead to undefined behavior.\nEach agent processes messages sequentially, and concurrent requests may interleave in unexpected ways.\nWait for each request to complete before sending the next one. Use separate agents or conversations for parallel processing.\n\nThe response format is controlled by the `streaming` field in the request body:\n- If `streaming=false` (default): Returns a complete LettaResponse with all messages\n- If `streaming=true`: Returns a Server-Sent Events (SSE) stream\n\nAdditional streaming options (only used when streaming=true):\n- `stream_tokens`: Stream individual tokens instead of complete steps\n- `include_pings`: Include keepalive pings to prevent connection timeouts\n- `background`: Process the request in the background", + "operationId": "send_message", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LettaStreamingRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LettaResponse" + } + }, + "text/event-stream": { + "description": "Server-Sent Events stream (when streaming=true in request body)" + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/messages/{message_id}": { + "patch": { + "tags": ["agents"], + "summary": "Modify Message", + "description": "Update the details of a message associated with an agent.\n\n**Deprecated**: Messages are now considered immutable since they can be shared across\nmultiple conversations via forking. This endpoint will be removed in a future version.", + "operationId": "modify_message", + "deprecated": true, + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "message_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^message-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the message in the format 'message-'", + "examples": ["message-123e4567-e89b-42d3-8456-426614174000"], + "title": "Message Id" + }, + "description": "The ID of the message in the format 'message-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/UpdateSystemMessage" + }, + { + "$ref": "#/components/schemas/UpdateUserMessage" + }, + { + "$ref": "#/components/schemas/UpdateReasoningMessage" + }, + { + "$ref": "#/components/schemas/UpdateAssistantMessage" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/SystemMessage" + }, + { + "$ref": "#/components/schemas/UserMessage" + }, + { + "$ref": "#/components/schemas/ReasoningMessage" + }, + { + "$ref": "#/components/schemas/HiddenReasoningMessage" + }, + { + "$ref": "#/components/schemas/ToolCallMessage" + }, + { + "$ref": "#/components/schemas/ToolReturnMessage" + }, + { + "$ref": "#/components/schemas/AssistantMessage" + }, + { + "$ref": "#/components/schemas/ApprovalRequestMessage" + }, + { + "$ref": "#/components/schemas/ApprovalResponseMessage" + }, + { + "$ref": "#/components/schemas/SummaryMessage" + }, + { + "$ref": "#/components/schemas/EventMessage" + } + ], + "discriminator": { + "propertyName": "message_type", + "mapping": { + "system_message": "#/components/schemas/SystemMessage", + "user_message": "#/components/schemas/UserMessage", + "reasoning_message": "#/components/schemas/ReasoningMessage", + "hidden_reasoning_message": "#/components/schemas/HiddenReasoningMessage", + "tool_call_message": "#/components/schemas/ToolCallMessage", + "tool_return_message": "#/components/schemas/ToolReturnMessage", + "assistant_message": "#/components/schemas/AssistantMessage", + "approval_request_message": "#/components/schemas/ApprovalRequestMessage", + "approval_response_message": "#/components/schemas/ApprovalResponseMessage", + "summary_message": "#/components/schemas/SummaryMessage", + "event_message": "#/components/schemas/EventMessage" + } + }, + "title": "Response Modify Message" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/messages/stream": { + "post": { + "tags": ["agents"], + "summary": "Send Message Streaming", + "description": "Process a user message and return the agent's response.\n\nDeprecated: Use the `POST /{agent_id}/messages` endpoint with `streaming=true` in the request body instead.\n\n**Note:** Sending multiple concurrent requests to the same agent can lead to undefined behavior.\nEach agent processes messages sequentially, and concurrent requests may interleave in unexpected ways.\nWait for each request to complete before sending the next one. Use separate agents or conversations for parallel processing.\n\nThis endpoint accepts a message from a user and processes it through the agent.\nIt will stream the steps of the response always, and stream the tokens if 'stream_tokens' is set to True.", + "operationId": "create_agent_message_stream", + "deprecated": true, + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LettaStreamingRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LettaStreamingResponse" + } + }, + "text/event-stream": { + "description": "Server-Sent Events stream" + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/messages/cancel": { + "post": { + "tags": ["agents"], + "summary": "Cancel Message", + "description": "Cancel runs associated with an agent. If run_ids are passed in, cancel those in particular.\n\nNote to cancel active runs associated with an agent, redis is required.", + "operationId": "cancel_message", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelAgentRunRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Cancel Message" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/generate": { + "post": { + "tags": ["agents"], + "summary": "Generate Completion", + "description": "Generate a completion directly from the LLM provider using the agent's configuration.\n\nThis endpoint makes a direct request to the LLM provider without any agent processing:\n- No memory or context retrieval\n- No tool calling\n- No message persistence\n- No agent state modification\n\nSimply provide a prompt, and the endpoint formats it as a user message.\nOptionally include a system_prompt for context/instructions.\n\nThe agent's LLM configuration (model, credentials, settings) is used by default.\nUse override_model to switch to a different model/provider while still using\nthe organization's configured providers.\n\nExample use cases:\n- Quick LLM queries without agent overhead\n- Testing different models with the same prompt\n- Simple chat completions using agent's credentials\n- Comparing model outputs on identical prompts", + "operationId": "generate_completion", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenerateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful generation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenerateResponse" + } + } + } + }, + "404": { + "description": "Agent not found" + }, + "422": { + "description": "Invalid request parameters" + }, + "502": { + "description": "LLM provider error" + } + } + } + }, + "/v1/agents/messages/search": { + "post": { + "tags": ["agents"], + "summary": "Search Messages", + "description": "Search messages across the entire organization with optional project and template filtering. Returns messages with FTS/vector ranks and total RRF score.\n\nThis is a cloud-only feature.", + "operationId": "search_messages", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSearchRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageSearchResult" + }, + "title": "Response Search Messages" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/messages/async": { + "post": { + "tags": ["agents"], + "summary": "Send Message Async", + "description": "Asynchronously process a user message and return a run object.\nThe actual processing happens in the background, and the status can be checked using the run ID.\n\nThis is \"asynchronous\" in the sense that it's a background run and explicitly must be fetched by the run ID.\n\n**Note:** Sending multiple concurrent requests to the same agent can lead to undefined behavior.\nEach agent processes messages sequentially, and concurrent requests may interleave in unexpected ways.\nWait for each request to complete before sending the next one. Use separate agents or conversations for parallel processing.", + "operationId": "create_agent_message_async", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LettaAsyncRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/reset-messages": { + "patch": { + "tags": ["agents"], + "summary": "Reset Messages", + "description": "Resets the messages for an agent", + "operationId": "reset_messages", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResetMessagesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentState" + }, + { + "type": "null" + } + ], + "title": "Response Reset Messages" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/groups": { + "get": { + "tags": ["agents"], + "summary": "List Groups For Agent", + "description": "Lists the groups for an agent.", + "operationId": "list_groups_for_agent", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "manager_type", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Manager type to filter groups by", + "title": "Manager Type" + }, + "description": "Manager type to filter groups by" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Group ID cursor for pagination. Returns groups that come before this group ID in the specified sort order", + "title": "Before" + }, + "description": "Group ID cursor for pagination. Returns groups that come before this group ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Group ID cursor for pagination. Returns groups that come after this group ID in the specified sort order", + "title": "After" + }, + "description": "Group ID cursor for pagination. Returns groups that come after this group ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of groups to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of groups to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for groups by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for groups by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Group" + }, + "title": "Response List Groups For Agent" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/messages/preview-raw-payload": { + "post": { + "tags": ["agents"], + "summary": "Preview Model Request", + "description": "Inspect the raw LLM request payload without sending it.\n\nThis endpoint processes the message through the agent loop up until\nthe LLM request, then returns the raw request payload that would\nbe sent to the LLM provider. Useful for debugging and inspection.", + "operationId": "preview_model_request", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/LettaRequest" + }, + { + "$ref": "#/components/schemas/LettaStreamingRequest" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Preview Model Request" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/summarize": { + "post": { + "tags": ["agents"], + "summary": "Summarize Messages", + "description": "Summarize an agent's conversation history.", + "operationId": "summarize_messages", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/letta__server__rest_api__routers__v1__agents__CompactionRequest" + }, + { + "type": "null" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompactionResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/conversations/": { + "post": { + "tags": ["conversations"], + "summary": "Create Conversation", + "description": "Create a new conversation for an agent.", + "operationId": "create_conversation", + "parameters": [ + { + "name": "agent_id", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "The agent ID to create a conversation for", + "title": "Agent Id" + }, + "description": "The agent ID to create a conversation for" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateConversation" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["conversations"], + "summary": "List Conversations", + "description": "List all conversations for an agent (or all conversations if agent_id not provided).", + "operationId": "list_conversations", + "parameters": [ + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The agent ID to list conversations for (optional - returns all conversations if not provided)", + "title": "Agent Id" + }, + "description": "The agent ID to list conversations for (optional - returns all conversations if not provided)" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "description": "Maximum number of conversations to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of conversations to return" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Cursor for pagination (conversation ID)", + "title": "After" + }, + "description": "Cursor for pagination (conversation ID)" + }, + { + "name": "summary_search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search for text within conversation summaries", + "title": "Summary Search" + }, + "description": "Search for text within conversation summaries" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for conversations. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for conversations. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "enum": ["created_at", "last_run_completion", "last_message_at"], + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Conversation" + }, + "title": "Response List Conversations" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/conversations/{conversation_id}": { + "get": { + "tags": ["conversations"], + "summary": "Retrieve Conversation", + "description": "Retrieve a specific conversation.", + "operationId": "retrieve_conversation", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the conv in the format 'conv-'", + "examples": ["conv-123e4567-e89b-42d3-8456-426614174000"], + "title": "Conversation Id" + }, + "description": "The ID of the conv in the format 'conv-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["conversations"], + "summary": "Update Conversation", + "description": "Update a conversation.", + "operationId": "update_conversation", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the conv in the format 'conv-'", + "examples": ["conv-123e4567-e89b-42d3-8456-426614174000"], + "title": "Conversation Id" + }, + "description": "The ID of the conv in the format 'conv-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateConversation" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["conversations"], + "summary": "Delete Conversation", + "description": "Delete a conversation (soft delete).\n\nThis marks the conversation as deleted but does not permanently remove it from the database.\nThe conversation will no longer appear in list operations.\nAny isolated blocks associated with the conversation will be permanently deleted.", + "operationId": "delete_conversation", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the conv in the format 'conv-'", + "examples": ["conv-123e4567-e89b-42d3-8456-426614174000"], + "title": "Conversation Id" + }, + "description": "The ID of the conv in the format 'conv-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/conversations/{conversation_id}/fork": { + "post": { + "tags": ["conversations"], + "summary": "Fork Conversation", + "description": "Fork an existing conversation.\n\nCreates a new conversation that shares the same in-context messages as the source\nconversation, but with a newly compiled system message reflecting the latest memory\nblock values. The forked conversation belongs to the same agent as the source.\n\n**Agent-direct mode**: Pass conversation_id=\"default\" with agent_id query parameter\nto fork the agent's default (agent-direct) message history into a new conversation.\n\n**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.", + "operationId": "fork_conversation", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 42, + "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated).", + "examples": [ + "default", + "conv-123e4567-e89b-42d3-8456-426614174000", + "agent-123e4567-e89b-42d3-8456-426614174000" + ], + "title": "Conversation Id" + }, + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated)." + }, + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID for agent-direct mode with 'default' conversation", + "title": "Agent Id" + }, + "description": "Agent ID for agent-direct mode with 'default' conversation" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/conversations/{conversation_id}/messages": { + "get": { + "tags": ["conversations"], + "summary": "List Conversation Messages", + "description": "List all messages in a conversation.\n\nReturns LettaMessage objects (UserMessage, AssistantMessage, etc.) for all\nmessages in the conversation, with support for cursor-based pagination.\n\n**Agent-direct mode**: Pass conversation_id=\"default\" with agent_id parameter\nto list messages from the agent's default conversation.\n\n**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.", + "operationId": "list_conversation_messages", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 42, + "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated).", + "examples": [ + "default", + "conv-123e4567-e89b-42d3-8456-426614174000", + "agent-123e4567-e89b-42d3-8456-426614174000" + ], + "title": "Conversation Id" + }, + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated)." + }, + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID for agent-direct mode with 'default' conversation", + "title": "Agent Id" + }, + "description": "Agent ID for agent-direct mode with 'default' conversation" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order", + "title": "Before" + }, + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order", + "title": "After" + }, + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of messages to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of messages to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "group_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Group ID to filter messages by.", + "title": "Group Id" + }, + "description": "Group ID to filter messages by." + }, + { + "name": "include_err", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether to include error messages and error statuses. For debugging purposes only.", + "title": "Include Err" + }, + "description": "Whether to include error messages and error statuses. For debugging purposes only." + }, + { + "name": "include_return_message_types", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageType" + } + }, + { + "type": "null" + } + ], + "description": "Message types to include in response. When null, all message types are returned.", + "title": "Include Return Message Types" + }, + "description": "Message types to include in response. When null, all message types are returned." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LettaMessageUnion" + }, + "title": "Response List Conversation Messages" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["conversations"], + "summary": "Send Conversation Message", + "description": "Send a message to a conversation and get a response.\n\nThis endpoint sends a message to an existing conversation.\nBy default (streaming=true), returns a streaming response (Server-Sent Events).\nSet streaming=false to get a complete JSON response.\n\n**Agent-direct mode**: Pass conversation_id=\"default\" with agent_id in request body\nto send messages to the agent's default conversation with locking.\n\n**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.", + "operationId": "send_conversation_message", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 42, + "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated).", + "examples": [ + "default", + "conv-123e4567-e89b-42d3-8456-426614174000", + "agent-123e4567-e89b-42d3-8456-426614174000" + ], + "title": "Conversation Id" + }, + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated)." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationMessageRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LettaResponse" + }, + "description": "JSON response (when streaming=false)" + }, + "text/event-stream": { + "description": "Server-Sent Events stream (default, when streaming=true)" + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/conversations/{conversation_id}/messages/preview-raw-payload": { + "post": { + "tags": ["conversations"], + "summary": "Preview Conversation Model Request", + "description": "Inspect the raw LLM request payload for a conversation message without sending it.\n\nThis endpoint processes the message through the same path as send_conversation_message\n(including conversation-scoped messages, isolated blocks, model overrides, and\nclient tools/skills) but stops before the LLM call and returns the raw request\npayload. Useful for debugging and verifying what the LLM will actually see.", + "operationId": "preview_conversation_model_request", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 42, + "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated).", + "examples": [ + "default", + "conv-123e4567-e89b-42d3-8456-426614174000", + "agent-123e4567-e89b-42d3-8456-426614174000" + ], + "title": "Conversation Id" + }, + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated)." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationMessageRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Preview Conversation Model Request" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/conversations/{conversation_id}/stream": { + "post": { + "tags": ["conversations"], + "summary": "Retrieve Conversation Stream", + "description": "Resume the stream for the most recent active run in a conversation.\n\nThis endpoint allows you to reconnect to an active background stream\nfor a conversation, enabling recovery from network interruptions.\n\n**Agent-direct mode**: Pass conversation_id=\"default\" with agent_id in request body\nto retrieve the stream for the agent's most recent active run.\n\n**Direct run access**: Pass run_id directly to skip run lookup entirely.\nUseful for recovery from duplicate request 409 errors.\n\n**OTID lookup**: Pass otid to look up the run_id from Redis.\nUseful when you have the otid from a 409 error response.\n\n**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.", + "operationId": "retrieve_conversation_stream", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 42, + "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated).", + "examples": [ + "default", + "conv-123e4567-e89b-42d3-8456-426614174000", + "agent-123e4567-e89b-42d3-8456-426614174000" + ], + "title": "Conversation Id" + }, + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated)." + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RetrieveStreamRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": {} + }, + "text/event-stream": { + "description": "Server-Sent Events stream", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/SystemMessage" + }, + { + "$ref": "#/components/schemas/UserMessage" + }, + { + "$ref": "#/components/schemas/ReasoningMessage" + }, + { + "$ref": "#/components/schemas/HiddenReasoningMessage" + }, + { + "$ref": "#/components/schemas/ToolCallMessage" + }, + { + "$ref": "#/components/schemas/ToolReturnMessage" + }, + { + "$ref": "#/components/schemas/AssistantMessage" + }, + { + "$ref": "#/components/schemas/ApprovalRequestMessage" + }, + { + "$ref": "#/components/schemas/ApprovalResponseMessage" + }, + { + "$ref": "#/components/schemas/LettaPing" + }, + { + "$ref": "#/components/schemas/LettaErrorMessage" + }, + { + "$ref": "#/components/schemas/LettaStopReason" + }, + { + "$ref": "#/components/schemas/LettaUsageStatistics" + } + ] + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/conversations/{conversation_id}/cancel": { + "post": { + "tags": ["conversations"], + "summary": "Cancel Conversation", + "description": "Cancel runs associated with a conversation.\n\nNote: To cancel active runs, Redis is required.\n\n**Agent-direct mode**: Pass conversation_id=\"default\" with agent_id query parameter\nto cancel runs for the agent's default conversation.\n\n**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.", + "operationId": "cancel_conversation", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 42, + "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated).", + "examples": [ + "default", + "conv-123e4567-e89b-42d3-8456-426614174000", + "agent-123e4567-e89b-42d3-8456-426614174000" + ], + "title": "Conversation Id" + }, + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated)." + }, + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID for agent-direct mode with 'default' conversation", + "title": "Agent Id" + }, + "description": "Agent ID for agent-direct mode with 'default' conversation" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Cancel Conversation" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/conversations/{conversation_id}/recompile": { + "post": { + "tags": ["conversations"], + "summary": "Recompile Conversation", + "description": "Manually trigger system prompt recompilation for a conversation.", + "operationId": "recompile_conversation", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 42, + "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated).", + "examples": [ + "default", + "conv-123e4567-e89b-42d3-8456-426614174000", + "agent-123e4567-e89b-42d3-8456-426614174000" + ], + "title": "Conversation Id" + }, + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated)." + }, + { + "name": "dry_run", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, do not persist changes; still returns the compiled system prompt.", + "default": false, + "title": "Dry Run" + }, + "description": "If True, do not persist changes; still returns the compiled system prompt." + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/letta__server__rest_api__routers__v1__conversations__CompactionRequest" + }, + { + "type": "null" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string", + "title": "Response Recompile Conversation" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/conversations/{conversation_id}/compact": { + "post": { + "tags": ["conversations"], + "summary": "Compact Conversation", + "description": "Compact (summarize) a conversation's message history.\n\nThis endpoint summarizes the in-context messages for a specific conversation,\nreducing the message count while preserving important context.\n\n**Agent-direct mode**: Pass conversation_id=\"default\" with agent_id in request body\nto compact the agent's default conversation messages.\n\n**Deprecated**: Passing an agent ID as conversation_id still works but will be removed.", + "operationId": "compact_conversation", + "parameters": [ + { + "name": "conversation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 42, + "pattern": "^(default|conv-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$", + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated).", + "examples": [ + "default", + "conv-123e4567-e89b-42d3-8456-426614174000", + "agent-123e4567-e89b-42d3-8456-426614174000" + ], + "title": "Conversation Id" + }, + "description": "The conversation identifier. Can be a conversation ID ('conv-'), 'default' for agent-direct mode (with agent_id parameter), or an agent ID ('agent-') for backwards compatibility (deprecated)." + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/letta__server__rest_api__routers__v1__conversations__CompactionRequest" + }, + { + "type": "null" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompactionResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/chat/completions": { + "post": { + "tags": ["chat"], + "summary": "Create Chat Completion", + "description": "Create a chat completion using a Letta agent (OpenAI-compatible).\n\nThis endpoint provides full OpenAI API compatibility. The agent is selected based on:\n- The 'model' parameter in the request (should contain an agent ID in format 'agent-...')\n\nWhen streaming is enabled (stream=true), the response will be Server-Sent Events\nwith ChatCompletionChunk objects.", + "operationId": "create_chat_completion", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatCompletionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatCompletion" + } + }, + "text/event-stream": { + "description": "Server-Sent Events stream (when stream=true)" + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/groups/": { + "get": { + "tags": ["groups"], + "summary": "List Groups", + "description": "Fetch all multi-agent groups matching query.", + "operationId": "list_groups", + "deprecated": true, + "parameters": [ + { + "name": "manager_type", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagerType" + }, + { + "type": "null" + } + ], + "description": "Search groups by manager type", + "title": "Manager Type" + }, + "description": "Search groups by manager type" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Group ID cursor for pagination. Returns groups that come before this group ID in the specified sort order", + "title": "Before" + }, + "description": "Group ID cursor for pagination. Returns groups that come before this group ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Group ID cursor for pagination. Returns groups that come after this group ID in the specified sort order", + "title": "After" + }, + "description": "Group ID cursor for pagination. Returns groups that come after this group ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of groups to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of groups to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for groups by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "asc", + "title": "Order" + }, + "description": "Sort order for groups by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "project_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search groups by project id", + "title": "Project Id" + }, + "description": "Search groups by project id" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Group" + }, + "title": "Response List Groups" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["groups"], + "summary": "Create Group", + "description": "Create a new multi-agent group with the specified configuration.", + "operationId": "create_group", + "deprecated": true, + "parameters": [ + { + "name": "X-Project", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The project slug to associate with the group (cloud only).", + "title": "X-Project" + }, + "description": "The project slug to associate with the group (cloud only)." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Group" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/groups/count": { + "get": { + "tags": ["groups"], + "summary": "Count Groups", + "description": "Get the count of all groups associated with a given user.", + "operationId": "count_groups", + "deprecated": true, + "parameters": [], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "integer", + "title": "Response Count Groups" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/groups/{group_id}": { + "get": { + "tags": ["groups"], + "summary": "Retrieve Group", + "description": "Retrieve the group by id.", + "operationId": "retrieve_group", + "deprecated": true, + "parameters": [ + { + "name": "group_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^group-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the group in the format 'group-'", + "examples": ["group-123e4567-e89b-42d3-8456-426614174000"], + "title": "Group Id" + }, + "description": "The ID of the group in the format 'group-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Group" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["groups"], + "summary": "Modify Group", + "description": "Create a new multi-agent group with the specified configuration.", + "operationId": "modify_group", + "deprecated": true, + "parameters": [ + { + "name": "group_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^group-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the group in the format 'group-'", + "examples": ["group-123e4567-e89b-42d3-8456-426614174000"], + "title": "Group Id" + }, + "description": "The ID of the group in the format 'group-'" + }, + { + "name": "X-Project", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The project slug to associate with the group (cloud only).", + "title": "X-Project" + }, + "description": "The project slug to associate with the group (cloud only)." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Group" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["groups"], + "summary": "Delete Group", + "description": "Delete a multi-agent group.", + "operationId": "delete_group", + "deprecated": true, + "parameters": [ + { + "name": "group_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^group-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the group in the format 'group-'", + "examples": ["group-123e4567-e89b-42d3-8456-426614174000"], + "title": "Group Id" + }, + "description": "The ID of the group in the format 'group-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/groups/{group_id}/messages/{message_id}": { + "patch": { + "tags": ["groups"], + "summary": "Modify Group Message", + "description": "Update the details of a message associated with an agent.", + "operationId": "modify_group_message", + "deprecated": true, + "parameters": [ + { + "name": "group_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^group-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the group in the format 'group-'", + "examples": ["group-123e4567-e89b-42d3-8456-426614174000"], + "title": "Group Id" + }, + "description": "The ID of the group in the format 'group-'" + }, + { + "name": "message_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^message-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the message in the format 'message-'", + "examples": ["message-123e4567-e89b-42d3-8456-426614174000"], + "title": "Message Id" + }, + "description": "The ID of the message in the format 'message-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/UpdateSystemMessage" + }, + { + "$ref": "#/components/schemas/UpdateUserMessage" + }, + { + "$ref": "#/components/schemas/UpdateReasoningMessage" + }, + { + "$ref": "#/components/schemas/UpdateAssistantMessage" + } + ], + "title": "Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/SystemMessage" + }, + { + "$ref": "#/components/schemas/UserMessage" + }, + { + "$ref": "#/components/schemas/ReasoningMessage" + }, + { + "$ref": "#/components/schemas/HiddenReasoningMessage" + }, + { + "$ref": "#/components/schemas/ToolCallMessage" + }, + { + "$ref": "#/components/schemas/ToolReturnMessage" + }, + { + "$ref": "#/components/schemas/AssistantMessage" + }, + { + "$ref": "#/components/schemas/ApprovalRequestMessage" + }, + { + "$ref": "#/components/schemas/ApprovalResponseMessage" + }, + { + "$ref": "#/components/schemas/SummaryMessage" + }, + { + "$ref": "#/components/schemas/EventMessage" + } + ], + "discriminator": { + "propertyName": "message_type", + "mapping": { + "system_message": "#/components/schemas/SystemMessage", + "user_message": "#/components/schemas/UserMessage", + "reasoning_message": "#/components/schemas/ReasoningMessage", + "hidden_reasoning_message": "#/components/schemas/HiddenReasoningMessage", + "tool_call_message": "#/components/schemas/ToolCallMessage", + "tool_return_message": "#/components/schemas/ToolReturnMessage", + "assistant_message": "#/components/schemas/AssistantMessage", + "approval_request_message": "#/components/schemas/ApprovalRequestMessage", + "approval_response_message": "#/components/schemas/ApprovalResponseMessage", + "summary_message": "#/components/schemas/SummaryMessage", + "event_message": "#/components/schemas/EventMessage" + } + }, + "title": "Response Modify Group Message" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/groups/{group_id}/messages": { + "get": { + "tags": ["groups"], + "summary": "List Group Messages", + "description": "Retrieve message history for an agent.", + "operationId": "list_group_messages", + "deprecated": true, + "parameters": [ + { + "name": "group_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^group-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the group in the format 'group-'", + "examples": ["group-123e4567-e89b-42d3-8456-426614174000"], + "title": "Group Id" + }, + "description": "The ID of the group in the format 'group-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order", + "title": "Before" + }, + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order", + "title": "After" + }, + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of messages to retrieve", + "default": 10, + "title": "Limit" + }, + "description": "Maximum number of messages to retrieve" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "use_assistant_message", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to use assistant messages", + "deprecated": true, + "default": true, + "title": "Use Assistant Message" + }, + "description": "Whether to use assistant messages", + "deprecated": true + }, + { + "name": "assistant_message_tool_name", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "The name of the designated message tool.", + "deprecated": true, + "default": "send_message", + "title": "Assistant Message Tool Name" + }, + "description": "The name of the designated message tool.", + "deprecated": true + }, + { + "name": "assistant_message_tool_kwarg", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "The name of the message argument.", + "deprecated": true, + "default": "message", + "title": "Assistant Message Tool Kwarg" + }, + "description": "The name of the message argument.", + "deprecated": true + }, + { + "name": "include_return_message_types", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageType" + } + }, + { + "type": "null" + } + ], + "description": "Message types to include in response. When null, all message types are returned.", + "title": "Include Return Message Types" + }, + "description": "Message types to include in response. When null, all message types are returned." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LettaMessageUnion" + }, + "title": "Response List Group Messages" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/groups/{group_id}/reset-messages": { + "patch": { + "tags": ["groups"], + "summary": "Reset Group Messages", + "description": "Delete the group messages for all agents that are part of the multi-agent group.", + "operationId": "reset_group_messages", + "deprecated": true, + "parameters": [ + { + "name": "group_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^group-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the group in the format 'group-'", + "examples": ["group-123e4567-e89b-42d3-8456-426614174000"], + "title": "Group Id" + }, + "description": "The ID of the group in the format 'group-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/groups/{group_id}/blocks/attach/{block_id}": { + "patch": { + "tags": ["groups"], + "summary": "Attach Block To Group", + "description": "Attach a block to a group.\nThis will add the block to the group and all agents within the group.", + "operationId": "attach_block_to_group", + "deprecated": true, + "parameters": [ + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Block Id" + } + }, + { + "name": "group_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^group-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the group in the format 'group-'", + "examples": ["group-123e4567-e89b-42d3-8456-426614174000"], + "title": "Group Id" + }, + "description": "The ID of the group in the format 'group-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/groups/{group_id}/blocks/detach/{block_id}": { + "patch": { + "tags": ["groups"], + "summary": "Detach Block From Group", + "description": "Detach a block from a group.\nThis will remove the block from the group and all agents within the group.", + "operationId": "detach_block_from_group", + "deprecated": true, + "parameters": [ + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Block Id" + } + }, + { + "name": "group_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^group-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the group in the format 'group-'", + "examples": ["group-123e4567-e89b-42d3-8456-426614174000"], + "title": "Group Id" + }, + "description": "The ID of the group in the format 'group-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/identities/": { + "get": { + "tags": ["identities", "identities"], + "summary": "List Identities", + "description": "Get a list of all identities in the database", + "operationId": "list_identities", + "deprecated": true, + "parameters": [ + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + } + }, + { + "name": "project_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "[DEPRECATED: Use X-Project-Id header instead] Filter identities by project ID", + "deprecated": true, + "title": "Project Id" + }, + "description": "[DEPRECATED: Use X-Project-Id header instead] Filter identities by project ID", + "deprecated": true + }, + { + "name": "identifier_key", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Identifier Key" + } + }, + { + "name": "identity_type", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/IdentityType" + }, + { + "type": "null" + } + ], + "title": "Identity Type" + } + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Identity ID cursor for pagination. Returns identities that come before this identity ID in the specified sort order", + "title": "Before" + }, + "description": "Identity ID cursor for pagination. Returns identities that come before this identity ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Identity ID cursor for pagination. Returns identities that come after this identity ID in the specified sort order", + "title": "After" + }, + "description": "Identity ID cursor for pagination. Returns identities that come after this identity ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of identities to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of identities to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for identities by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for identities by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Identity" + }, + "title": "Response List Identities" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["identities", "identities"], + "summary": "Create Identity", + "operationId": "create_identity", + "deprecated": true, + "parameters": [ + { + "name": "X-Project", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The project slug to associate with the identity (cloud only).", + "title": "X-Project" + }, + "description": "The project slug to associate with the identity (cloud only)." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Identity" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "put": { + "tags": ["identities", "identities"], + "summary": "Upsert Identity", + "operationId": "upsert_identity", + "deprecated": true, + "parameters": [ + { + "name": "X-Project", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The project slug to associate with the identity (cloud only).", + "title": "X-Project" + }, + "description": "The project slug to associate with the identity (cloud only)." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityUpsert" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Identity" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/identities/count": { + "get": { + "tags": ["identities", "identities"], + "summary": "Count Identities", + "description": "Get count of all identities for a user", + "operationId": "count_identities", + "deprecated": true, + "parameters": [], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "integer", + "title": "Response Count Identities" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/identities/{identity_id}": { + "get": { + "tags": ["identities", "identities"], + "summary": "Retrieve Identity", + "operationId": "retrieve_identity", + "deprecated": true, + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], + "title": "Identity Id" + }, + "description": "The ID of the identity in the format 'identity-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Identity" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["identities", "identities"], + "summary": "Modify Identity", + "operationId": "update_identity", + "deprecated": true, + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], + "title": "Identity Id" + }, + "description": "The ID of the identity in the format 'identity-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Identity" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["identities", "identities"], + "summary": "Delete Identity", + "description": "Delete an identity by its identifier key", + "operationId": "delete_identity", + "deprecated": true, + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], + "title": "Identity Id" + }, + "description": "The ID of the identity in the format 'identity-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/identities/{identity_id}/properties": { + "put": { + "tags": ["identities", "identities"], + "summary": "Upsert Properties For Identity", + "operationId": "upsert_properties_for_identity", + "deprecated": true, + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], + "title": "Identity Id" + }, + "description": "The ID of the identity in the format 'identity-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IdentityProperty" + }, + "title": "Properties" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/identities/{identity_id}/agents": { + "get": { + "tags": ["identities"], + "summary": "List Agents For Identity", + "description": "Get all agents associated with the specified identity.", + "operationId": "list_agents_for_identity", + "deprecated": true, + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], + "title": "Identity Id" + }, + "description": "The ID of the identity in the format 'identity-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order", + "title": "Before" + }, + "description": "Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order", + "title": "After" + }, + "description": "Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of agents to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of agents to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "enum": [ + "agent.blocks", + "agent.identities", + "agent.managed_group", + "agent.pending_approval", + "agent.secrets", + "agent.sources", + "agent.tags", + "agent.tools" + ], + "type": "string" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default.", + "default": [], + "title": "Include" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentState" + }, + "title": "Response List Agents For Identity" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/identities/{identity_id}/blocks": { + "get": { + "tags": ["identities"], + "summary": "List Blocks For Identity", + "description": "Get all blocks associated with the specified identity.", + "operationId": "list_blocks_for_identity", + "deprecated": true, + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], + "title": "Identity Id" + }, + "description": "The ID of the identity in the format 'identity-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order", + "title": "Before" + }, + "description": "Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order", + "title": "After" + }, + "description": "Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of blocks to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of blocks to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlockResponse" + }, + "title": "Response List Blocks For Identity" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_agents/count": { + "get": { + "tags": ["_internal_agents"], + "summary": "Count Agents", + "description": "Get the total number of agents for a user, with option to exclude hidden agents.", + "operationId": "count_internal_agents", + "parameters": [ + { + "name": "exclude_hidden", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, excludes hidden agents from the count. If False, includes all agents.", + "default": true, + "title": "Exclude Hidden" + }, + "description": "If True, excludes hidden agents from the count. If False, includes all agents." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "integer", + "title": "Response Count Internal Agents" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_agents/{agent_id}/core-memory/blocks/{block_label}": { + "patch": { + "tags": ["_internal_agents"], + "summary": "Modify Block For Agent", + "description": "Updates a core memory block of an agent.", + "operationId": "modify_internal_core_memory_block", + "parameters": [ + { + "name": "block_label", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Block Label" + } + }, + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Block" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_blocks/": { + "get": { + "tags": ["_internal_blocks"], + "summary": "List Blocks", + "operationId": "list_internal_blocks", + "parameters": [ + { + "name": "label", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 50, + "pattern": "^[a-zA-Z0-9_/-]+$" + }, + { + "type": "null" + } + ], + "description": "Label to include (alphanumeric, hyphens, underscores, forward slashes)", + "examples": [ + "human", + "persona", + "the_label_of-a-block", + "the_label_of-a-block/with-forward-slash" + ], + "title": "Label" + }, + "description": "Label to include (alphanumeric, hyphens, underscores, forward slashes)" + }, + { + "name": "templates_only", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to include only templates", + "default": false, + "title": "Templates Only" + }, + "description": "Whether to include only templates" + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 100, + "pattern": "^[a-zA-Z0-9 _-]+$" + }, + { + "type": "null" + } + ], + "description": "Name filter (alphanumeric, spaces, hyphens, underscores)", + "examples": ["My Agent", "test_tool", "default-config"], + "title": "Name" + }, + "description": "Name filter (alphanumeric, spaces, hyphens, underscores)" + }, + { + "name": "identity_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + { + "type": "null" + } + ], + "description": "The ID of the identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], + "title": "Identity Id" + }, + "description": "The ID of the identity in the format 'identity-'" + }, + { + "name": "identifier_keys", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Search agents by identifier keys", + "title": "Identifier Keys" + }, + "description": "Search agents by identifier keys" + }, + { + "name": "project_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search blocks by project id", + "title": "Project Id" + }, + "description": "Search blocks by project id" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Number of blocks to return", + "default": 50, + "title": "Limit" + }, + "description": "Number of blocks to return" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order", + "title": "Before" + }, + "description": "Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order", + "title": "After" + }, + "description": "Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "asc", + "title": "Order" + }, + "description": "Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "label_search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 50, + "pattern": "^[a-zA-Z0-9_/-]+$" + }, + { + "type": "null" + } + ], + "description": "Search blocks by label. If provided, returns blocks whose label matches the search query. This is a full-text search on block labels.", + "examples": [ + "human", + "persona", + "the_label_of-a-block", + "the_label_of-a-block/with-forward-slash" + ], + "title": "Label Search" + }, + "description": "Search blocks by label. If provided, returns blocks whose label matches the search query. This is a full-text search on block labels." + }, + { + "name": "description_search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + { + "type": "null" + } + ], + "description": "Search blocks by description. If provided, returns blocks whose description matches the search query. This is a full-text search on block descriptions.", + "title": "Description Search" + }, + "description": "Search blocks by description. If provided, returns blocks whose description matches the search query. This is a full-text search on block descriptions." + }, + { + "name": "value_search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + { + "type": "null" + } + ], + "description": "Search blocks by value. If provided, returns blocks whose value matches the search query. This is a full-text search on block values.", + "title": "Value Search" + }, + "description": "Search blocks by value. If provided, returns blocks whose value matches the search query. This is a full-text search on block values." + }, + { + "name": "connected_to_agents_count_gt", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Filter blocks by the number of connected agents. If provided, returns blocks that have more than this number of connected agents.", + "title": "Connected To Agents Count Gt" + }, + "description": "Filter blocks by the number of connected agents. If provided, returns blocks that have more than this number of connected agents." + }, + { + "name": "connected_to_agents_count_lt", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Filter blocks by the number of connected agents. If provided, returns blocks that have less than this number of connected agents.", + "title": "Connected To Agents Count Lt" + }, + "description": "Filter blocks by the number of connected agents. If provided, returns blocks that have less than this number of connected agents." + }, + { + "name": "connected_to_agents_count_eq", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Filter blocks by the exact number of connected agents. If provided, returns blocks that have exactly this number of connected agents.", + "title": "Connected To Agents Count Eq" + }, + "description": "Filter blocks by the exact number of connected agents. If provided, returns blocks that have exactly this number of connected agents." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Block" + }, + "title": "Response List Internal Blocks" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["_internal_blocks"], + "summary": "Create Block", + "operationId": "create_internal_block", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBlock" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Block" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_blocks/{block_id}": { + "delete": { + "tags": ["_internal_blocks"], + "summary": "Delete Block", + "operationId": "delete_internal_block", + "parameters": [ + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_blocks/{block_id}/agents": { + "get": { + "tags": ["_internal_blocks"], + "summary": "List Agents For Block", + "description": "Retrieves all agents associated with the specified block.\nRaises a 404 if the block does not exist.", + "operationId": "list_agents_for_internal_block", + "parameters": [ + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order", + "title": "Before" + }, + "description": "Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order", + "title": "After" + }, + "description": "Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of agents to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of agents to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "include_relationships", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. If not provided, all relationships are loaded by default. Using this can optimize performance by reducing unnecessary joins.This is a legacy parameter, and no longer supported after 1.0.0 SDK versions.", + "deprecated": true, + "title": "Include Relationships" + }, + "description": "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. If not provided, all relationships are loaded by default. Using this can optimize performance by reducing unnecessary joins.This is a legacy parameter, and no longer supported after 1.0.0 SDK versions.", + "deprecated": true + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default.", + "default": [], + "title": "Include" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentState" + }, + "title": "Response List Agents For Internal Block" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_runs/": { + "get": { + "tags": ["_internal_runs"], + "summary": "List Runs", + "description": "List all runs.", + "operationId": "list_internal_runs", + "parameters": [ + { + "name": "run_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter by a specific run ID.", + "title": "Run Id" + }, + "description": "Filter by a specific run ID." + }, + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The unique identifier of the agent associated with the run.", + "title": "Agent Id" + }, + "description": "The unique identifier of the agent associated with the run." + }, + { + "name": "agent_ids", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "The unique identifiers of the agents associated with the run. Deprecated in favor of agent_id field.", + "deprecated": true, + "title": "Agent Ids" + }, + "description": "The unique identifiers of the agents associated with the run. Deprecated in favor of agent_id field.", + "deprecated": true + }, + { + "name": "statuses", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter runs by status. Can specify multiple statuses.", + "title": "Statuses" + }, + "description": "Filter runs by status. Can specify multiple statuses." + }, + { + "name": "background", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "If True, filters for runs that were created in background mode.", + "title": "Background" + }, + "description": "If True, filters for runs that were created in background mode." + }, + { + "name": "stop_reason", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StopReasonType" + }, + { + "type": "null" + } + ], + "description": "Filter runs by stop reason.", + "title": "Stop Reason" + }, + "description": "Filter runs by stop reason." + }, + { + "name": "template_family", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter runs by template family (base_template_id).", + "title": "Template Family" + }, + "description": "Filter runs by template family (base_template_id)." + }, + { + "name": "step_count", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Filter runs by step count. Must be provided with step_count_operator.", + "title": "Step Count" + }, + "description": "Filter runs by step count. Must be provided with step_count_operator." + }, + { + "name": "step_count_operator", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/ComparisonOperator", + "description": "Operator for step_count filter: 'eq' for equals, 'gte' for greater than or equal, 'lte' for less than or equal.", + "default": "eq" + }, + "description": "Operator for step_count filter: 'eq' for equals, 'gte' for greater than or equal, 'lte' for less than or equal." + }, + { + "name": "tools_used", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter runs that used any of the specified tools.", + "title": "Tools Used" + }, + "description": "Filter runs that used any of the specified tools." + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Run ID cursor for pagination. Returns runs that come before this run ID in the specified sort order", + "title": "Before" + }, + "description": "Run ID cursor for pagination. Returns runs that come before this run ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Run ID cursor for pagination. Returns runs that come after this run ID in the specified sort order", + "title": "After" + }, + "description": "Run ID cursor for pagination. Returns runs that come after this run ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer", + "maximum": 1000, + "minimum": 1 + }, + { + "type": "null" + } + ], + "description": "Maximum number of runs to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of runs to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for runs by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for runs by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "enum": ["created_at", "duration"], + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "active", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Filter for active runs.", + "default": false, + "title": "Active" + }, + "description": "Filter for active runs." + }, + { + "name": "ascending", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to sort agents oldest to newest (True) or newest to oldest (False, default). Deprecated in favor of order field.", + "deprecated": true, + "default": false, + "title": "Ascending" + }, + "description": "Whether to sort agents oldest to newest (True) or newest to oldest (False, default). Deprecated in favor of order field.", + "deprecated": true + }, + { + "name": "project_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter runs by project ID.", + "title": "Project Id" + }, + "description": "Filter runs by project ID." + }, + { + "name": "conversation_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter runs by conversation ID.", + "title": "Conversation Id" + }, + "description": "Filter runs by conversation ID." + }, + { + "name": "duration_percentile", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Filter runs by duration percentile (1-100). Returns runs slower than this percentile.", + "title": "Duration Percentile" + }, + "description": "Filter runs by duration percentile (1-100). Returns runs slower than this percentile." + }, + { + "name": "duration_value", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Duration value in nanoseconds for filtering. Must be used with duration_operator.", + "title": "Duration Value" + }, + "description": "Duration value in nanoseconds for filtering. Must be used with duration_operator." + }, + { + "name": "duration_operator", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "enum": ["gt", "lt", "eq"], + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Comparison operator for duration filter: 'gt' (greater than), 'lt' (less than), 'eq' (equals).", + "title": "Duration Operator" + }, + "description": "Comparison operator for duration filter: 'gt' (greater than), 'lt' (less than), 'eq' (equals)." + }, + { + "name": "start_date", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "description": "Filter runs created on or after this date (ISO 8601 format).", + "title": "Start Date" + }, + "description": "Filter runs created on or after this date (ISO 8601 format)." + }, + { + "name": "end_date", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "description": "Filter runs created on or before this date (ISO 8601 format).", + "title": "End Date" + }, + "description": "Filter runs created on or before this date (ISO 8601 format)." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Run" + }, + "title": "Response List Internal Runs" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_templates/groups": { + "post": { + "tags": ["_internal_templates"], + "summary": "Create Group", + "description": "Create a new multi-agent group with the specified configuration.", + "operationId": "create_internal_template_group", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalTemplateGroupCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Group" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_templates/agents": { + "post": { + "tags": ["_internal_templates"], + "summary": "Create Agent", + "description": "Create a new agent with template-related fields.", + "operationId": "create_internal_template_agent", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalTemplateAgentCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentState" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_templates/blocks": { + "post": { + "tags": ["_internal_templates"], + "summary": "Create Block", + "description": "Create a new block with template-related fields.", + "operationId": "create_internal_template_block", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalTemplateBlockCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Block" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_templates/blocks/batch": { + "post": { + "tags": ["_internal_templates"], + "summary": "Create Blocks Batch", + "description": "Create multiple blocks with template-related fields.", + "operationId": "create_internal_template_blocks_batch", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InternalTemplateBlockCreate" + }, + "title": "Blocks" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Block" + }, + "title": "Response Create Internal Template Blocks Batch" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/_internal_templates/deployment/{deployment_id}": { + "get": { + "tags": ["_internal_templates"], + "summary": "List Deployment Entities", + "description": "List all entities (blocks, agents, groups) with the specified deployment_id.\nOptionally filter by entity types.", + "operationId": "list_deployment_entities", + "parameters": [ + { + "name": "deployment_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Deployment Id" + } + }, + { + "name": "entity_types", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter by entity types (block, agent, group)", + "title": "Entity Types" + }, + "description": "Filter by entity types (block, agent, group)" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListDeploymentEntitiesResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["_internal_templates"], + "summary": "Delete Deployment", + "description": "Delete all entities (blocks, agents, groups) with the specified deployment_id.\nDeletion order: blocks -> agents -> groups to maintain referential integrity.", + "operationId": "delete_deployment", + "parameters": [ + { + "name": "deployment_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Deployment Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteDeploymentResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/models/": { + "get": { + "tags": ["models", "llms"], + "summary": "List Llm Models", + "description": "List available LLM models using the asynchronous implementation for improved performance.\n\nReturns Model format which extends LLMConfig with additional metadata fields.\nLegacy LLMConfig fields are marked as deprecated but still available for backward compatibility.", + "operationId": "list_models", + "parameters": [ + { + "name": "provider_category", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProviderCategory" + } + }, + { + "type": "null" + } + ], + "title": "Provider Category" + } + }, + { + "name": "provider_name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider Name" + } + }, + { + "name": "provider_type", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderType" + }, + { + "type": "null" + } + ], + "title": "Provider Type" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model" + }, + "title": "Response List Models" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/models/embedding": { + "get": { + "tags": ["models", "llms"], + "summary": "List Embedding Models", + "description": "List available embedding models using the asynchronous implementation for improved performance.\n\nReturns EmbeddingModel format which extends EmbeddingConfig with additional metadata fields.\nLegacy EmbeddingConfig fields are marked as deprecated but still available for backward compatibility.", + "operationId": "list_embedding_models", + "parameters": [], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EmbeddingModel" + }, + "title": "Response List Embedding Models" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/mcp-servers/": { + "post": { + "tags": ["mcp-servers"], + "summary": "Create Mcp Server", + "description": "Add a new MCP server to the Letta MCP server config", + "operationId": "mcp_create_mcp_server", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMCPServerRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StdioMCPServer" + }, + { + "$ref": "#/components/schemas/SSEMCPServer" + }, + { + "$ref": "#/components/schemas/StreamableHTTPMCPServer" + } + ], + "title": "Response Mcp Create Mcp Server" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["mcp-servers"], + "summary": "List Mcp Servers", + "description": "Get a list of all configured MCP servers", + "operationId": "mcp_list_mcp_servers", + "parameters": [], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/StdioMCPServer" + }, + { + "$ref": "#/components/schemas/SSEMCPServer" + }, + { + "$ref": "#/components/schemas/StreamableHTTPMCPServer" + } + ] + }, + "title": "Response Mcp List Mcp Servers" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/mcp-servers/{mcp_server_id}": { + "get": { + "tags": ["mcp-servers"], + "summary": "Retrieve Mcp Server", + "description": "Get a specific MCP server", + "operationId": "mcp_retrieve_mcp_server", + "parameters": [ + { + "name": "mcp_server_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StdioMCPServer" + }, + { + "$ref": "#/components/schemas/SSEMCPServer" + }, + { + "$ref": "#/components/schemas/StreamableHTTPMCPServer" + } + ], + "title": "Response Mcp Retrieve Mcp Server" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["mcp-servers"], + "summary": "Delete Mcp Server", + "description": "Delete an MCP server by its ID", + "operationId": "mcp_delete_mcp_server", + "parameters": [ + { + "name": "mcp_server_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Id" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["mcp-servers"], + "summary": "Update Mcp Server", + "description": "Update an existing MCP server configuration", + "operationId": "mcp_update_mcp_server", + "parameters": [ + { + "name": "mcp_server_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMCPServerRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StdioMCPServer" + }, + { + "$ref": "#/components/schemas/SSEMCPServer" + }, + { + "$ref": "#/components/schemas/StreamableHTTPMCPServer" + } + ], + "title": "Response Mcp Update Mcp Server" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/mcp-servers/{mcp_server_id}/tools": { + "get": { + "tags": ["mcp-servers"], + "summary": "List Tools For Mcp Server", + "description": "Get a list of all tools for a specific MCP server", + "operationId": "mcp_list_tools_for_mcp_server", + "parameters": [ + { + "name": "mcp_server_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Tool" + }, + "title": "Response Mcp List Tools For Mcp Server" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/mcp-servers/{mcp_server_id}/tools/{tool_id}": { + "get": { + "tags": ["mcp-servers"], + "summary": "Retrieve Mcp Tool", + "description": "Get a specific MCP tool by its ID", + "operationId": "mcp_retrieve_mcp_tool", + "parameters": [ + { + "name": "mcp_server_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Id" + } + }, + { + "name": "tool_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Tool Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Tool" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/mcp-servers/{mcp_server_id}/tools/{tool_id}/run": { + "post": { + "tags": ["mcp-servers"], + "summary": "Run Mcp Tool", + "description": "Execute a specific MCP tool\n\nThe request body should contain the tool arguments in the ToolExecuteRequest format.", + "operationId": "mcp_run_tool", + "parameters": [ + { + "name": "mcp_server_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Id" + } + }, + { + "name": "tool_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Tool Id" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/letta__schemas__mcp_server__ToolExecuteRequest", + "default": { + "args": {} + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolExecutionResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/mcp-servers/{mcp_server_id}/refresh": { + "patch": { + "tags": ["mcp-servers"], + "summary": "Refresh Mcp Server Tools", + "description": "Refresh tools for an MCP server by:\n1. Fetching current tools from the MCP server\n2. Deleting tools that no longer exist on the server\n3. Updating schemas for existing tools\n4. Adding new tools from the server\n\nReturns a summary of changes made.", + "operationId": "mcp_refresh_mcp_server_tools", + "parameters": [ + { + "name": "mcp_server_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Id" + } + }, + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/mcp-servers/connect/{mcp_server_id}": { + "get": { + "tags": ["mcp-servers"], + "summary": "Connect Mcp Server", + "description": "Connect to an MCP server with support for OAuth via SSE.\nReturns a stream of events handling authorization state and exchange if OAuth is required.", + "operationId": "mcp_connect_mcp_server", + "parameters": [ + { + "name": "mcp_server_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Mcp Server Id" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": {} + }, + "text/event-stream": { + "description": "Server-Sent Events stream" + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/blocks/": { + "get": { + "tags": ["blocks"], + "summary": "List Blocks", + "operationId": "list_blocks", + "parameters": [ + { + "name": "label", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 50, + "pattern": "^[a-zA-Z0-9_/-]+$" + }, + { + "type": "null" + } + ], + "description": "Label to include (alphanumeric, hyphens, underscores, forward slashes)", + "examples": [ + "human", + "persona", + "the_label_of-a-block", + "the_label_of-a-block/with-forward-slash" + ], + "title": "Label" + }, + "description": "Label to include (alphanumeric, hyphens, underscores, forward slashes)" + }, + { + "name": "templates_only", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to include only templates", + "default": false, + "title": "Templates Only" + }, + "description": "Whether to include only templates" + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 100, + "pattern": "^[a-zA-Z0-9 _-]+$" + }, + { + "type": "null" + } + ], + "description": "Name filter (alphanumeric, spaces, hyphens, underscores)", + "examples": ["My Agent", "test_tool", "default-config"], + "title": "Name" + }, + "description": "Name filter (alphanumeric, spaces, hyphens, underscores)" + }, + { + "name": "identity_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + { + "type": "null" + } + ], + "description": "The ID of the identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], + "title": "Identity Id" + }, + "description": "The ID of the identity in the format 'identity-'" + }, + { + "name": "identifier_keys", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Search agents by identifier keys", + "title": "Identifier Keys" + }, + "description": "Search agents by identifier keys" + }, + { + "name": "project_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search blocks by project id", + "title": "Project Id" + }, + "description": "Search blocks by project id" + }, + { + "name": "tags", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "List of tags to filter blocks by", + "title": "Tags" + }, + "description": "List of tags to filter blocks by" + }, + { + "name": "match_all_tags", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, only returns blocks that match ALL given tags. Otherwise, return blocks that have ANY of the passed-in tags.", + "default": false, + "title": "Match All Tags" + }, + "description": "If True, only returns blocks that match ALL given tags. Otherwise, return blocks that have ANY of the passed-in tags." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Number of blocks to return", + "default": 50, + "title": "Limit" + }, + "description": "Number of blocks to return" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order", + "title": "Before" + }, + "description": "Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order", + "title": "After" + }, + "description": "Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "asc", + "title": "Order" + }, + "description": "Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "label_search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 50, + "pattern": "^[a-zA-Z0-9_/-]+$" + }, + { + "type": "null" + } + ], + "description": "Search blocks by label. If provided, returns blocks whose label matches the search query. This is a full-text search on block labels.", + "examples": [ + "human", + "persona", + "the_label_of-a-block", + "the_label_of-a-block/with-forward-slash" + ], + "title": "Label Search" + }, + "description": "Search blocks by label. If provided, returns blocks whose label matches the search query. This is a full-text search on block labels." + }, + { + "name": "description_search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + { + "type": "null" + } + ], + "description": "Search blocks by description. If provided, returns blocks whose description matches the search query. This is a full-text search on block descriptions.", + "title": "Description Search" + }, + "description": "Search blocks by description. If provided, returns blocks whose description matches the search query. This is a full-text search on block descriptions." + }, + { + "name": "value_search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + { + "type": "null" + } + ], + "description": "Search blocks by value. If provided, returns blocks whose value matches the search query. This is a full-text search on block values.", + "title": "Value Search" + }, + "description": "Search blocks by value. If provided, returns blocks whose value matches the search query. This is a full-text search on block values." + }, + { + "name": "connected_to_agents_count_gt", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Filter blocks by the number of connected agents. If provided, returns blocks that have more than this number of connected agents.", + "title": "Connected To Agents Count Gt" + }, + "description": "Filter blocks by the number of connected agents. If provided, returns blocks that have more than this number of connected agents." + }, + { + "name": "connected_to_agents_count_lt", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Filter blocks by the number of connected agents. If provided, returns blocks that have less than this number of connected agents.", + "title": "Connected To Agents Count Lt" + }, + "description": "Filter blocks by the number of connected agents. If provided, returns blocks that have less than this number of connected agents." + }, + { + "name": "connected_to_agents_count_eq", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "integer" + } + }, + { + "type": "null" + } + ], + "description": "Filter blocks by the exact number of connected agents. If provided, returns blocks that have exactly this number of connected agents.", + "title": "Connected To Agents Count Eq" + }, + "description": "Filter blocks by the exact number of connected agents. If provided, returns blocks that have exactly this number of connected agents." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlockResponse" + }, + "title": "Response List Blocks" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["blocks"], + "summary": "Create Block", + "operationId": "create_block", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBlock" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/blocks/count": { + "get": { + "tags": ["blocks"], + "summary": "Count Blocks", + "description": "Count all blocks with optional filtering.\nSupports the same filters as list_blocks for consistent querying.", + "operationId": "count_blocks", + "parameters": [ + { + "name": "label", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 50, + "pattern": "^[a-zA-Z0-9_/-]+$" + }, + { + "type": "null" + } + ], + "description": "Label to include (alphanumeric, hyphens, underscores, forward slashes)", + "examples": [ + "human", + "persona", + "the_label_of-a-block", + "the_label_of-a-block/with-forward-slash" + ], + "title": "Label" + }, + "description": "Label to include (alphanumeric, hyphens, underscores, forward slashes)" + }, + { + "name": "templates_only", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to include only templates", + "default": false, + "title": "Templates Only" + }, + "description": "Whether to include only templates" + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 100, + "pattern": "^[a-zA-Z0-9 _-]+$" + }, + { + "type": "null" + } + ], + "description": "Name filter (alphanumeric, spaces, hyphens, underscores)", + "examples": ["My Agent", "test_tool", "default-config"], + "title": "Name" + }, + "description": "Name filter (alphanumeric, spaces, hyphens, underscores)" + }, + { + "name": "tags", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "List of tags to filter blocks by", + "title": "Tags" + }, + "description": "List of tags to filter blocks by" + }, + { + "name": "match_all_tags", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If True, only counts blocks that match ALL given tags. Otherwise, counts blocks that have ANY of the passed-in tags.", + "default": false, + "title": "Match All Tags" + }, + "description": "If True, only counts blocks that match ALL given tags. Otherwise, counts blocks that have ANY of the passed-in tags." + }, + { + "name": "project_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search blocks by project id", + "title": "Project Id" + }, + "description": "Search blocks by project id" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "integer", + "title": "Response Count Blocks" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/blocks/{block_id}": { + "patch": { + "tags": ["blocks"], + "summary": "Modify Block", + "operationId": "modify_block", + "parameters": [ + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["blocks"], + "summary": "Delete Block", + "operationId": "delete_block", + "parameters": [ + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["blocks"], + "summary": "Retrieve Block", + "operationId": "retrieve_block", + "parameters": [ + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/blocks/{block_id}/agents": { + "get": { + "tags": ["blocks"], + "summary": "List Agents For Block", + "description": "Retrieves all agents associated with the specified block.\nRaises a 404 if the block does not exist.", + "operationId": "list_agents_for_block", + "parameters": [ + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order", + "title": "Before" + }, + "description": "Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order", + "title": "After" + }, + "description": "Agent ID cursor for pagination. Returns agents that come after this agent ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of agents to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of agents to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for agents by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "include_relationships", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. If not provided, all relationships are loaded by default. Using this can optimize performance by reducing unnecessary joins.This is a legacy parameter, and no longer supported after 1.0.0 SDK versions.", + "deprecated": true, + "title": "Include Relationships" + }, + "description": "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. If not provided, all relationships are loaded by default. Using this can optimize performance by reducing unnecessary joins.This is a legacy parameter, and no longer supported after 1.0.0 SDK versions.", + "deprecated": true + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "enum": [ + "agent.blocks", + "agent.identities", + "agent.managed_group", + "agent.pending_approval", + "agent.secrets", + "agent.sources", + "agent.tags", + "agent.tools" + ], + "type": "string" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default.", + "default": [], + "title": "Include" + }, + "description": "Specify which relational fields to include in the response. No relationships are included by default." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentState" + }, + "title": "Response List Agents For Block" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/blocks/{block_id}/identities/attach/{identity_id}": { + "patch": { + "tags": ["blocks"], + "summary": "Attach Identity To Block", + "description": "Attach an identity to a block.", + "operationId": "attach_identity_to_block", + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Identity Id" + } + }, + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/blocks/{block_id}/identities/detach/{identity_id}": { + "patch": { + "tags": ["blocks"], + "summary": "Detach Identity From Block", + "description": "Detach an identity from a block.", + "operationId": "detach_identity_from_block", + "parameters": [ + { + "name": "identity_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Identity Id" + } + }, + { + "name": "block_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"], + "title": "Block Id" + }, + "description": "The ID of the block in the format 'block-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/jobs/": { + "get": { + "tags": ["jobs"], + "summary": "List Jobs", + "description": "List all jobs.", + "operationId": "list_jobs", + "parameters": [ + { + "name": "source_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use `folder_id` parameter instead. Only list jobs associated with the source.", + "deprecated": true, + "title": "Source Id" + }, + "description": "Deprecated: Use `folder_id` parameter instead. Only list jobs associated with the source.", + "deprecated": true + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Job ID cursor for pagination. Returns jobs that come before this job ID in the specified sort order", + "title": "Before" + }, + "description": "Job ID cursor for pagination. Returns jobs that come before this job ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Job ID cursor for pagination. Returns jobs that come after this job ID in the specified sort order", + "title": "After" + }, + "description": "Job ID cursor for pagination. Returns jobs that come after this job ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of jobs to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of jobs to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for jobs by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for jobs by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "active", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Filter for active jobs.", + "default": false, + "title": "Active" + }, + "description": "Filter for active jobs." + }, + { + "name": "ascending", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to sort jobs oldest to newest (True, default) or newest to oldest (False). Deprecated in favor of order field.", + "deprecated": true, + "default": true, + "title": "Ascending" + }, + "description": "Whether to sort jobs oldest to newest (True, default) or newest to oldest (False). Deprecated in favor of order field.", + "deprecated": true + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Job" + }, + "title": "Response List Jobs" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/jobs/active": { + "get": { + "tags": ["jobs"], + "summary": "List Active Jobs", + "description": "List all active jobs.", + "operationId": "list_active_jobs", + "deprecated": true, + "parameters": [ + { + "name": "source_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use `folder_id` parameter instead. Only list jobs associated with the source.", + "deprecated": true, + "title": "Source Id" + }, + "description": "Deprecated: Use `folder_id` parameter instead. Only list jobs associated with the source.", + "deprecated": true + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Cursor for pagination", + "title": "Before" + }, + "description": "Cursor for pagination" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Cursor for pagination", + "title": "After" + }, + "description": "Cursor for pagination" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Limit for pagination", + "default": 50, + "title": "Limit" + }, + "description": "Limit for pagination" + }, + { + "name": "ascending", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to sort jobs oldest to newest (True, default) or newest to oldest (False)", + "default": true, + "title": "Ascending" + }, + "description": "Whether to sort jobs oldest to newest (True, default) or newest to oldest (False)" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Job" + }, + "title": "Response List Active Jobs" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/jobs/{job_id}": { + "get": { + "tags": ["jobs"], + "summary": "Retrieve Job", + "description": "Get the status of a job.", + "operationId": "retrieve_job", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 40, + "maxLength": 40, + "pattern": "^job-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the job in the format 'job-'", + "examples": ["job-123e4567-e89b-42d3-8456-426614174000"], + "title": "Job Id" + }, + "description": "The ID of the job in the format 'job-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Job" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["jobs"], + "summary": "Delete Job", + "description": "Delete a job by its job_id.", + "operationId": "delete_job", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 40, + "maxLength": 40, + "pattern": "^job-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the job in the format 'job-'", + "examples": ["job-123e4567-e89b-42d3-8456-426614174000"], + "title": "Job Id" + }, + "description": "The ID of the job in the format 'job-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Job" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/jobs/{job_id}/cancel": { + "patch": { + "tags": ["jobs"], + "summary": "Cancel Job", + "description": "Cancel a job by its job_id.\n\nThis endpoint marks a job as cancelled, which will cause any associated\nagent execution to terminate as soon as possible.", + "operationId": "cancel_job", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 40, + "maxLength": 40, + "pattern": "^job-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the job in the format 'job-'", + "examples": ["job-123e4567-e89b-42d3-8456-426614174000"], + "title": "Job Id" + }, + "description": "The ID of the job in the format 'job-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Job" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/health/": { + "get": { + "tags": ["health"], + "summary": "Check Health", + "description": "Liveness endpoint; returns 200 when process is responsive.", + "operationId": "check_health", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Health" + } + } + } + } + } + } + }, + "/v1/ready/": { + "get": { + "tags": ["health"], + "summary": "Check Readiness", + "description": "Readiness endpoint gated by internal readiness state when enforcement is enabled.", + "operationId": "check_readiness", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Health" + } + } + } + } + } + } + }, + "/v1/providers/": { + "get": { + "tags": ["providers"], + "summary": "List Providers", + "description": "Get a list of all custom providers.", + "operationId": "list_providers", + "parameters": [ + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Provider ID cursor for pagination. Returns providers that come before this provider ID in the specified sort order", + "title": "Before" + }, + "description": "Provider ID cursor for pagination. Returns providers that come before this provider ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Provider ID cursor for pagination. Returns providers that come after this provider ID in the specified sort order", + "title": "After" + }, + "description": "Provider ID cursor for pagination. Returns providers that come after this provider ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of providers to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of providers to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for providers by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for providers by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter providers by name", + "title": "Name" + }, + "description": "Filter providers by name" + }, + { + "name": "provider_type", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderType" + }, + { + "type": "null" + } + ], + "description": "Filter providers by type", + "title": "Provider Type" + }, + "description": "Filter providers by type" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Provider" + }, + "title": "Response List Providers" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": ["providers"], + "summary": "Create Provider", + "description": "Create a new custom provider.", + "operationId": "create_provider", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/providers/{provider_id}": { + "get": { + "tags": ["providers"], + "summary": "Retrieve Provider", + "description": "Get a provider by ID.", + "operationId": "retrieve_provider", + "parameters": [ + { + "name": "provider_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^provider-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the provider in the format 'provider-'", + "examples": ["provider-123e4567-e89b-42d3-8456-426614174000"], + "title": "Provider Id" + }, + "description": "The ID of the provider in the format 'provider-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "tags": ["providers"], + "summary": "Modify Provider", + "description": "Update an existing custom provider.", + "operationId": "modify_provider", + "parameters": [ + { + "name": "provider_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^provider-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the provider in the format 'provider-'", + "examples": ["provider-123e4567-e89b-42d3-8456-426614174000"], + "title": "Provider Id" + }, + "description": "The ID of the provider in the format 'provider-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["providers"], + "summary": "Delete Provider", + "description": "Delete an existing custom provider.", + "operationId": "delete_provider", + "parameters": [ + { + "name": "provider_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^provider-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the provider in the format 'provider-'", + "examples": ["provider-123e4567-e89b-42d3-8456-426614174000"], + "title": "Provider Id" + }, + "description": "The ID of the provider in the format 'provider-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/providers/check": { + "post": { + "tags": ["providers"], + "summary": "Check Provider", + "description": "Verify the API key and additional parameters for a provider.", + "operationId": "check_provider", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderCheck" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/providers/{provider_id}/check": { + "post": { + "tags": ["providers"], + "summary": "Check Existing Provider", + "description": "Verify the API key and additional parameters for an existing provider.", + "operationId": "check_existing_provider", + "parameters": [ + { + "name": "provider_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^provider-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the provider in the format 'provider-'", + "examples": ["provider-123e4567-e89b-42d3-8456-426614174000"], + "title": "Provider Id" + }, + "description": "The ID of the provider in the format 'provider-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/providers/{provider_id}/refresh": { + "patch": { + "tags": ["providers"], + "summary": "Refresh Provider Models", + "description": "Refresh models for a BYOK provider by querying the provider's API.\nAdds new models and removes ones no longer available.", + "operationId": "refresh_provider_models", + "parameters": [ + { + "name": "provider_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^provider-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the provider in the format 'provider-'", + "examples": ["provider-123e4567-e89b-42d3-8456-426614174000"], + "title": "Provider Id" + }, + "description": "The ID of the provider in the format 'provider-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/": { + "get": { + "tags": ["runs"], + "summary": "List Runs", + "description": "List all runs.", + "operationId": "list_runs", + "parameters": [ + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The unique identifier of the agent associated with the run.", + "title": "Agent Id" + }, + "description": "The unique identifier of the agent associated with the run." + }, + { + "name": "agent_ids", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "The unique identifiers of the agents associated with the run. Deprecated in favor of agent_id field.", + "deprecated": true, + "title": "Agent Ids" + }, + "description": "The unique identifiers of the agents associated with the run. Deprecated in favor of agent_id field.", + "deprecated": true + }, + { + "name": "statuses", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter runs by status. Can specify multiple statuses.", + "title": "Statuses" + }, + "description": "Filter runs by status. Can specify multiple statuses." + }, + { + "name": "background", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "If True, filters for runs that were created in background mode.", + "title": "Background" + }, + "description": "If True, filters for runs that were created in background mode." + }, + { + "name": "stop_reason", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/StopReasonType" + }, + { + "type": "null" + } + ], + "description": "Filter runs by stop reason.", + "title": "Stop Reason" + }, + "description": "Filter runs by stop reason." + }, + { + "name": "conversation_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter runs by conversation ID.", + "title": "Conversation Id" + }, + "description": "Filter runs by conversation ID." + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Run ID cursor for pagination. Returns runs that come before this run ID in the specified sort order", + "title": "Before" + }, + "description": "Run ID cursor for pagination. Returns runs that come before this run ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Run ID cursor for pagination. Returns runs that come after this run ID in the specified sort order", + "title": "After" + }, + "description": "Run ID cursor for pagination. Returns runs that come after this run ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer", + "maximum": 1000, + "minimum": 1 + }, + { + "type": "null" + } + ], + "description": "Maximum number of runs to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of runs to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for runs by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for runs by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "active", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Filter for active runs.", + "default": false, + "title": "Active" + }, + "description": "Filter for active runs." + }, + { + "name": "ascending", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to sort agents oldest to newest (True) or newest to oldest (False, default). Deprecated in favor of order field.", + "deprecated": true, + "default": false, + "title": "Ascending" + }, + "description": "Whether to sort agents oldest to newest (True) or newest to oldest (False, default). Deprecated in favor of order field.", + "deprecated": true + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Run" + }, + "title": "Response List Runs" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/active": { + "get": { + "tags": ["runs"], + "summary": "List Active Runs", + "description": "List all active runs.", + "operationId": "list_active_runs", + "deprecated": true, + "parameters": [ + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The unique identifier of the agent associated with the run.", + "title": "Agent Id" + }, + "description": "The unique identifier of the agent associated with the run." + }, + { + "name": "background", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "If True, filters for runs that were created in background mode.", + "title": "Background" + }, + "description": "If True, filters for runs that were created in background mode." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Run" + }, + "title": "Response List Active Runs" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}": { + "get": { + "tags": ["runs"], + "summary": "Retrieve Run", + "description": "Get the status of a run.", + "operationId": "retrieve_run", + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Run Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Run" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["runs"], + "summary": "Delete Run", + "description": "Delete a run by its run_id.", + "operationId": "delete_run", + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Run Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/messages": { + "get": { + "tags": ["runs"], + "summary": "List Messages For Run", + "description": "Get response messages associated with a run.", + "operationId": "list_messages_for_run", + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Run Id" + } + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order", + "title": "Before" + }, + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order", + "title": "After" + }, + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of messages to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of messages to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "asc", + "title": "Order" + }, + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LettaMessageUnion" + }, + "title": "Response List Messages For Run" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/usage": { + "get": { + "tags": ["runs"], + "summary": "Retrieve Usage For Run", + "description": "Get usage statistics for a run.", + "operationId": "retrieve_usage_for_run", + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Run Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsageStatistics" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/metrics": { + "get": { + "tags": ["runs"], + "summary": "Retrieve Metrics For Run", + "description": "Get run metrics by run ID.", + "operationId": "retrieve_metrics_for_run", + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Run Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunMetrics" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/steps": { + "get": { + "tags": ["runs"], + "summary": "List Steps For Run", + "description": "Get steps associated with a run with filtering options.", + "operationId": "list_steps_for_run", + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Run Id" + } + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Cursor for pagination", + "title": "Before" + }, + "description": "Cursor for pagination" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Cursor for pagination", + "title": "After" + }, + "description": "Cursor for pagination" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of messages to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of messages to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for steps by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for steps by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Step" + }, + "title": "Response List Steps For Run" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/trace": { + "get": { + "tags": ["runs"], + "summary": "Retrieve Trace For Run", + "description": "Retrieve OTEL trace spans for a run.\n\nReturns a filtered set of spans relevant for observability:\n- agent_step: Individual agent reasoning steps\n- tool executions: Tool call spans\n- Root span: The top-level request span\n- time_to_first_token: TTFT measurement span\n\nRequires ClickHouse to be configured for trace storage.", + "operationId": "retrieve_trace_for_run", + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Run Id" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 5000, + "minimum": 1, + "description": "Maximum number of spans to return", + "default": 1000, + "title": "Limit" + }, + "description": "Maximum number of spans to return" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + }, + "title": "Response Retrieve Trace For Run" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/runs/{run_id}/stream": { + "post": { + "tags": ["runs"], + "summary": "Retrieve Stream For Run", + "operationId": "retrieve_stream_for_run", + "parameters": [ + { + "name": "run_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Run Id" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RetrieveStreamRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": {} + }, + "text/event-stream": { + "description": "Server-Sent Events stream", + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/SystemMessage" + }, + { + "$ref": "#/components/schemas/UserMessage" + }, + { + "$ref": "#/components/schemas/ReasoningMessage" + }, + { + "$ref": "#/components/schemas/HiddenReasoningMessage" + }, + { + "$ref": "#/components/schemas/ToolCallMessage" + }, + { + "$ref": "#/components/schemas/ToolReturnMessage" + }, + { + "$ref": "#/components/schemas/AssistantMessage" + }, + { + "$ref": "#/components/schemas/ApprovalRequestMessage" + }, + { + "$ref": "#/components/schemas/ApprovalResponseMessage" + }, + { + "$ref": "#/components/schemas/LettaPing" + }, + { + "$ref": "#/components/schemas/LettaErrorMessage" + }, + { + "$ref": "#/components/schemas/LettaStopReason" + }, + { + "$ref": "#/components/schemas/LettaUsageStatistics" + } + ] + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/steps/": { + "get": { + "tags": ["steps"], + "summary": "List Steps", + "description": "List steps with optional pagination and date filters.", + "operationId": "list_steps", + "parameters": [ + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Return steps before this step ID", + "title": "Before" + }, + "description": "Return steps before this step ID" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Return steps after this step ID", + "title": "After" + }, + "description": "Return steps after this step ID" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of steps to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of steps to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for steps by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for steps by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "start_date", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Return steps after this ISO datetime (e.g. \"2025-01-29T15:01:19-08:00\")", + "title": "Start Date" + }, + "description": "Return steps after this ISO datetime (e.g. \"2025-01-29T15:01:19-08:00\")" + }, + { + "name": "end_date", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Return steps before this ISO datetime (e.g. \"2025-01-29T15:01:19-08:00\")", + "title": "End Date" + }, + "description": "Return steps before this ISO datetime (e.g. \"2025-01-29T15:01:19-08:00\")" + }, + { + "name": "model", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter by the name of the model used for the step", + "title": "Model" + }, + "description": "Filter by the name of the model used for the step" + }, + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter by the ID of the agent that performed the step", + "title": "Agent Id" + }, + "description": "Filter by the ID of the agent that performed the step" + }, + { + "name": "trace_ids", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter by trace ids returned by the server", + "title": "Trace Ids" + }, + "description": "Filter by trace ids returned by the server" + }, + { + "name": "feedback", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "enum": ["positive", "negative"], + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter by feedback", + "title": "Feedback" + }, + "description": "Filter by feedback" + }, + { + "name": "has_feedback", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Filter by whether steps have feedback (true) or not (false)", + "title": "Has Feedback" + }, + "description": "Filter by whether steps have feedback (true) or not (false)" + }, + { + "name": "tags", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Filter by tags", + "title": "Tags" + }, + "description": "Filter by tags" + }, + { + "name": "project_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter by the project ID that is associated with the step (cloud only).", + "title": "Project Id" + }, + "description": "Filter by the project ID that is associated with the step (cloud only)." + }, + { + "name": "X-Project", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter by project slug to associate with the group (cloud only).", + "title": "X-Project" + }, + "description": "Filter by project slug to associate with the group (cloud only)." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Step" + }, + "title": "Response List Steps" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/steps/{step_id}": { + "get": { + "tags": ["steps"], + "summary": "Retrieve Step", + "description": "Get a step by ID.", + "operationId": "retrieve_step", + "parameters": [ + { + "name": "step_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^step-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the step in the format 'step-'", + "examples": ["step-123e4567-e89b-42d3-8456-426614174000"], + "title": "Step Id" + }, + "description": "The ID of the step in the format 'step-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Step" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/steps/{step_id}/metrics": { + "get": { + "tags": ["steps"], + "summary": "Retrieve Metrics For Step", + "description": "Get step metrics by step ID.", + "operationId": "retrieve_metrics_for_step", + "parameters": [ + { + "name": "step_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^step-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the step in the format 'step-'", + "examples": ["step-123e4567-e89b-42d3-8456-426614174000"], + "title": "Step Id" + }, + "description": "The ID of the step in the format 'step-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StepMetrics" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/steps/{step_id}/trace": { + "get": { + "tags": ["steps"], + "summary": "Retrieve Trace For Step", + "operationId": "retrieve_trace_for_step", + "parameters": [ + { + "name": "step_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^step-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the step in the format 'step-'", + "examples": ["step-123e4567-e89b-42d3-8456-426614174000"], + "title": "Step Id" + }, + "description": "The ID of the step in the format 'step-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderTrace" + }, + { + "type": "null" + } + ], + "title": "Response Retrieve Trace For Step" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/steps/{step_id}/feedback": { + "patch": { + "tags": ["steps"], + "summary": "Modify Feedback For Step", + "description": "Modify feedback for a given step.", + "operationId": "modify_feedback_for_step", + "parameters": [ + { + "name": "step_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^step-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the step in the format 'step-'", + "examples": ["step-123e4567-e89b-42d3-8456-426614174000"], + "title": "Step Id" + }, + "description": "The ID of the step in the format 'step-'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModifyFeedbackRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Step" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/steps/{step_id}/messages": { + "get": { + "tags": ["steps"], + "summary": "List Messages For Step", + "description": "List messages for a given step.", + "operationId": "list_messages_for_step", + "parameters": [ + { + "name": "step_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^step-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the step in the format 'step-'", + "examples": ["step-123e4567-e89b-42d3-8456-426614174000"], + "title": "Step Id" + }, + "description": "The ID of the step in the format 'step-'" + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order", + "title": "Before" + }, + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order", + "title": "After" + }, + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of messages to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of messages to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "asc", + "title": "Order" + }, + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Sort by field", + "default": "created_at", + "title": "Order By" + }, + "description": "Sort by field" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/SystemMessage" + }, + { + "$ref": "#/components/schemas/UserMessage" + }, + { + "$ref": "#/components/schemas/ReasoningMessage" + }, + { + "$ref": "#/components/schemas/HiddenReasoningMessage" + }, + { + "$ref": "#/components/schemas/ToolCallMessage" + }, + { + "$ref": "#/components/schemas/ToolReturnMessage" + }, + { + "$ref": "#/components/schemas/AssistantMessage" + }, + { + "$ref": "#/components/schemas/ApprovalRequestMessage" + }, + { + "$ref": "#/components/schemas/ApprovalResponseMessage" + }, + { + "$ref": "#/components/schemas/SummaryMessage" + }, + { + "$ref": "#/components/schemas/EventMessage" + } + ], + "discriminator": { + "propertyName": "message_type", + "mapping": { + "system_message": "#/components/schemas/SystemMessage", + "user_message": "#/components/schemas/UserMessage", + "reasoning_message": "#/components/schemas/ReasoningMessage", + "hidden_reasoning_message": "#/components/schemas/HiddenReasoningMessage", + "tool_call_message": "#/components/schemas/ToolCallMessage", + "tool_return_message": "#/components/schemas/ToolReturnMessage", + "assistant_message": "#/components/schemas/AssistantMessage", + "approval_request_message": "#/components/schemas/ApprovalRequestMessage", + "approval_response_message": "#/components/schemas/ApprovalResponseMessage", + "summary_message": "#/components/schemas/SummaryMessage", + "event_message": "#/components/schemas/EventMessage" + } + } + }, + "title": "Response List Messages For Step" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/tags/": { + "get": { + "tags": ["tag", "admin", "admin"], + "summary": "List Tags", + "description": "Get the list of all tags (from agents and blocks) that have been created.", + "operationId": "list_tags", + "parameters": [ + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Tag cursor for pagination. Returns tags that come before this tag in the specified sort order", + "title": "Before" + }, + "description": "Tag cursor for pagination. Returns tags that come before this tag in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Tag cursor for pagination. Returns tags that come after this tag in the specified sort order", + "title": "After" + }, + "description": "Tag cursor for pagination. Returns tags that come after this tag in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of tags to return", + "default": 50, + "title": "Limit" + }, + "description": "Maximum number of tags to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for tags. 'asc' for alphabetical order, 'desc' for reverse alphabetical order", + "default": "asc", + "title": "Order" + }, + "description": "Sort order for tags. 'asc' for alphabetical order, 'desc' for reverse alphabetical order" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "name", + "type": "string", + "description": "Field to sort by", + "default": "name", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "query_text", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter tags by text search. Deprecated, please use name field instead", + "deprecated": true, + "title": "Query Text" + }, + "description": "Filter tags by text search. Deprecated, please use name field instead", + "deprecated": true + }, + { + "name": "name", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter tags by name", + "title": "Name" + }, + "description": "Filter tags by name" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Response List Tags" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/telemetry/{step_id}": { + "get": { + "tags": ["telemetry"], + "summary": "Retrieve Provider Trace", + "description": "**DEPRECATED**: Use `GET /steps/{step_id}/trace` instead.\n\nRetrieve provider trace by step ID.", + "operationId": "retrieve_provider_trace", + "deprecated": true, + "parameters": [ + { + "name": "step_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Step Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderTrace" + }, + { + "type": "null" + } + ], + "title": "Response Retrieve Provider Trace" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/messages/": { + "get": { + "tags": ["messages"], + "summary": "List All Messages", + "description": "List messages across all agents for the current user.", + "operationId": "list_all_messages", + "parameters": [ + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order", + "title": "Before" + }, + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order", + "title": "After" + }, + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of messages to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of messages to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "conversation_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Conversation ID to filter messages by", + "title": "Conversation Id" + }, + "description": "Conversation ID to filter messages by" + }, + { + "name": "include_return_message_types", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageType" + } + }, + { + "type": "null" + } + ], + "description": "Message types to include in response. When null, all message types are returned.", + "title": "Include Return Message Types" + }, + "description": "Message types to include in response. When null, all message types are returned." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LettaMessageUnion" + }, + "title": "Response List All Messages" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/messages/search": { + "post": { + "tags": ["messages"], + "summary": "Search All Messages", + "description": "Search messages across the organization with optional agent filtering.\nReturns messages with FTS/vector ranks and total RRF score.\n\n\nThis is a cloud-only feature.", + "operationId": "search_all_messages", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchAllMessagesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/SystemMessageListResult" + }, + { + "$ref": "#/components/schemas/UserMessageListResult" + }, + { + "$ref": "#/components/schemas/ReasoningMessageListResult" + }, + { + "$ref": "#/components/schemas/AssistantMessageListResult" + } + ], + "discriminator": { + "propertyName": "message_type", + "mapping": { + "system_message": "#/components/schemas/SystemMessageListResult", + "user_message": "#/components/schemas/UserMessageListResult", + "reasoning_message": "#/components/schemas/ReasoningMessageListResult", + "assistant_message": "#/components/schemas/AssistantMessageListResult" + } + } + }, + "title": "Response Search All Messages" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/messages/batches": { + "post": { + "tags": ["messages"], + "summary": "Create Batch", + "description": "Submit a batch of agent runs for asynchronous processing.\n\nCreates a job that will fan out messages to all listed agents and process them in parallel.\nThe request will be rejected if it exceeds 256MB.", + "operationId": "create_batch", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBatch", + "description": "Messages and config for all agents" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchJob" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "get": { + "tags": ["messages"], + "summary": "List Batches", + "description": "List all batch runs.", + "operationId": "list_batches", + "parameters": [ + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Job ID cursor for pagination. Returns jobs that come before this job ID in the specified sort order", + "title": "Before" + }, + "description": "Job ID cursor for pagination. Returns jobs that come before this job ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Job ID cursor for pagination. Returns jobs that come after this job ID in the specified sort order", + "title": "After" + }, + "description": "Job ID cursor for pagination. Returns jobs that come after this job ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of jobs to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of jobs to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for jobs by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for jobs by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BatchJob" + }, + "title": "Response List Batches" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/messages/batches/{batch_id}": { + "get": { + "tags": ["messages"], + "summary": "Retrieve Batch", + "description": "Retrieve the status and details of a batch run.", + "operationId": "retrieve_batch", + "parameters": [ + { + "name": "batch_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Batch Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchJob" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/messages/batches/{batch_id}/messages": { + "get": { + "tags": ["messages"], + "summary": "List Messages For Batch", + "description": "Get response messages for a specific batch job.", + "operationId": "list_messages_for_batch", + "parameters": [ + { + "name": "batch_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Batch Id" + } + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order", + "title": "Before" + }, + "description": "Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order" + }, + { + "name": "after", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order", + "title": "After" + }, + "description": "Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum number of messages to return", + "default": 100, + "title": "Limit" + }, + "description": "Maximum number of messages to return" + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "enum": ["asc", "desc"], + "type": "string", + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first", + "default": "desc", + "title": "Order" + }, + "description": "Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first" + }, + { + "name": "order_by", + "in": "query", + "required": false, + "schema": { + "const": "created_at", + "type": "string", + "description": "Field to sort by", + "default": "created_at", + "title": "Order By" + }, + "description": "Field to sort by" + }, + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Filter messages by agent ID", + "title": "Agent Id" + }, + "description": "Filter messages by agent ID" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LettaBatchMessages" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/messages/batches/{batch_id}/cancel": { + "patch": { + "tags": ["messages"], + "summary": "Cancel Batch", + "description": "Cancel a batch run.", + "operationId": "cancel_batch", + "parameters": [ + { + "name": "batch_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Batch Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/messages/{message_id}": { + "get": { + "tags": ["messages"], + "summary": "Retrieve Message", + "description": "Retrieve a message by ID.", + "operationId": "retrieve_message", + "parameters": [ + { + "name": "message_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^message-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the message in the format 'message-'", + "examples": ["message-123e4567-e89b-42d3-8456-426614174000"], + "title": "Message Id" + }, + "description": "The ID of the message in the format 'message-'" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LettaMessageUnion" + }, + "title": "Response Retrieve Message" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/passages/search": { + "post": { + "tags": ["passages"], + "summary": "Search Passages", + "description": "Search passages across the organization with optional agent and archive filtering.\nReturns passages with relevance scores.\n\nThis endpoint supports semantic search through passages:\n- If neither agent_id nor archive_id is provided, searches ALL passages in the organization\n- If agent_id is provided, searches passages across all archives attached to that agent\n- If archive_id is provided, searches passages within that specific archive\n- If both are provided, agent_id takes precedence", + "operationId": "search_passages", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PassageSearchRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PassageSearchResult" + }, + "title": "Response Search Passages" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/voice-beta/{agent_id}/chat/completions": { + "post": { + "tags": ["voice"], + "summary": "Create Voice Chat Completions", + "description": "DEPRECATED: This voice-beta endpoint has been deprecated.\n\nThe voice functionality has been integrated into the main chat completions endpoint.\nPlease use the standard /v1/agents/{agent_id}/messages endpoint instead.\n\nThis endpoint will be removed in a future version.", + "operationId": "create_voice_chat_completions", + "deprecated": true, + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Agent Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Completion Request" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": {} + }, + "text/event-stream": {} + } + }, + "410": { + "description": "Endpoint deprecated", + "content": { + "application/json": { + "example": { + "detail": "This endpoint has been deprecated" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/embeddings/total_storage_size": { + "get": { + "tags": ["embeddings"], + "summary": "Get Embeddings Total Storage Size", + "description": "Get the total size of all embeddings in the database for a user in the storage unit given.", + "operationId": "get_total_storage_size", + "parameters": [ + { + "name": "storage-unit", + "in": "header", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "GB", + "title": "Storage Unit" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "number", + "title": "Response Get Total Storage Size" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/agents/search": { + "post": { + "description": "Search deployed agents", + "summary": "Search Deployed Agents", + "tags": ["agents"], + "parameters": [], + "operationId": "agents.searchDeployedAgents", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "search": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["version"] + }, + "value": { + "type": "string" + } + }, + "required": ["field", "value"] + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["name"] + }, + "operator": { + "type": "string", + "enum": ["eq", "contains"] + }, + "value": { + "type": "string" + } + }, + "required": ["field", "operator", "value"] + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["tags"] + }, + "operator": { + "type": "string", + "enum": ["contains"] + }, + "value": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["field", "operator", "value"] + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["identity"] + }, + "operator": { + "type": "string", + "enum": ["eq"] + }, + "value": { + "type": "string" + } + }, + "required": ["field", "operator", "value"] + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["templateName"] + }, + "operator": { + "type": "string", + "enum": ["eq"] + }, + "value": { + "type": "string" + } + }, + "required": ["field", "operator", "value"] + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["agentId"] + }, + "operator": { + "type": "string", + "enum": ["eq"] + }, + "value": { + "type": "string" + } + }, + "required": ["field", "operator", "value"] + } + ] + } + }, + "project_id": { + "type": "string" + }, + "combinator": { + "type": "string", + "enum": ["AND"] + }, + "limit": { + "type": "number" + }, + "after": { + "type": "string", + "nullable": true + }, + "sortBy": { + "type": "string", + "enum": ["created_at", "last_run_completion", "updated_at"] + }, + "ascending": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentState" + } + }, + "nextCursor": { + "type": "string", + "nullable": true + } + }, + "required": ["agents"] + } + } + } + } + } + } + }, + "/v1/agents/search/count": { + "get": { + "description": "Count deployed agents matching search criteria", + "summary": "Count Deployed Agents", + "tags": ["agents"], + "parameters": [ + { + "name": "search", + "in": "query", + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["version"] + }, + "value": { + "type": "string" + } + }, + "required": ["field", "value"] + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["name"] + }, + "operator": { + "type": "string", + "enum": ["eq", "contains"] + }, + "value": { + "type": "string" + } + }, + "required": ["field", "operator", "value"] + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["tags"] + }, + "operator": { + "type": "string", + "enum": ["contains"] + }, + "value": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["field", "operator", "value"] + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["identity"] + }, + "operator": { + "type": "string", + "enum": ["eq"] + }, + "value": { + "type": "string" + } + }, + "required": ["field", "operator", "value"] + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["templateName"] + }, + "operator": { + "type": "string", + "enum": ["eq"] + }, + "value": { + "type": "string" + } + }, + "required": ["field", "operator", "value"] + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["agentId"] + }, + "operator": { + "type": "string", + "enum": ["eq"] + }, + "value": { + "type": "string" + } + }, + "required": ["field", "operator", "value"] + } + ] + } + } + }, + { + "name": "project_id", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "combinator", + "in": "query", + "schema": { + "type": "string", + "enum": ["AND"] + } + } + ], + "operationId": "agents.countDeployedAgents", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "number" + } + }, + "required": ["count"] + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/core-memory/variables": { + "get": { + "description": "Get the variables associated with an agent", + "summary": "Retrieve Memory Variables", + "tags": ["agents"], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "agents.getAgentVariables", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "variables": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["variables"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "enum": ["Agent not found"] + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/models/embeddings": { + "get": { + "tags": ["models"], + "parameters": [], + "operationId": "models.listEmbeddingModels", + "responses": { + "200": { + "description": "200" + } + } + } + }, + "/v1/templates/{project_id}/{template_version}/agents": { + "post": { + "description": "Creates an Agent or multiple Agents from a template", + "summary": "Create Agents From Template", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + }, + { + "name": "template_version", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template version, formatted as {template-name}:{version-number} or {template-name}:latest. This endpoint is not available for self-hosted Letta." + } + ], + "operationId": "templates.createAgentsFromTemplate", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9-_ ]*$" + }, + "description": "The tags to assign to the agent" + }, + "agent_name": { + "type": "string", + "pattern": "^[a-zA-Z0-9-_ ]*$", + "description": "The name of the agent, optional otherwise a random one will be assigned" + }, + "initial_message_sequence": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["user", "system", "assistant"] + }, + "content": { + "type": "string" + }, + "name": { + "type": "string", + "nullable": true + }, + "otid": { + "type": "string", + "nullable": true + }, + "sender_id": { + "type": "string", + "nullable": true + }, + "batch_item_id": { + "type": "string", + "nullable": true + }, + "group_id": { + "type": "string", + "nullable": true + } + }, + "required": ["role", "content"] + }, + "description": "Set an initial sequence of messages, if not provided, the agent will start with the default message sequence, if an empty array is provided, the agent will start with no messages" + }, + "memory_variables": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The memory variables to assign to the agent" + }, + "tool_variables": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The tool variables to assign to the agent" + }, + "identity_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The identity ids to assign to the agent" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "201" + }, + "402": { + "description": "402", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "limit": { + "type": "number" + } + }, + "required": ["message", "limit"] + } + } + } + } + } + } + }, + "/v1/templates/{template_version}/agents": { + "post": { + "description": "Creates an Agent or multiple Agents from a template", + "summary": "Create Agents From Template", + "tags": ["templates"], + "parameters": [ + { + "name": "template_version", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template version, formatted as {template-name}:{version-number} or {template-name}:latest. This endpoint is not available for self-hosted Letta." + } + ], + "operationId": "templates.createAgentsFromTemplateNoProject", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9-_ ]*$" + }, + "description": "The tags to assign to the agent" + }, + "agent_name": { + "type": "string", + "pattern": "^[a-zA-Z0-9-_ ]*$", + "description": "The name of the agent, optional otherwise a random one will be assigned" + }, + "initial_message_sequence": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["user", "system", "assistant"] + }, + "content": { + "type": "string" + }, + "name": { + "type": "string", + "nullable": true + }, + "otid": { + "type": "string", + "nullable": true + }, + "sender_id": { + "type": "string", + "nullable": true + }, + "batch_item_id": { + "type": "string", + "nullable": true + }, + "group_id": { + "type": "string", + "nullable": true + } + }, + "required": ["role", "content"] + }, + "description": "Set an initial sequence of messages, if not provided, the agent will start with the default message sequence, if an empty array is provided, the agent will start with no messages" + }, + "memory_variables": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The memory variables to assign to the agent" + }, + "tool_variables": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The tool variables to assign to the agent" + }, + "identity_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The identity ids to assign to the agent" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of created agent IDs" + }, + "group_id": { + "type": "string", + "nullable": true, + "description": "Optional group ID if agents were created in a group" + }, + "deployment_id": { + "type": "string", + "description": "The deployment ID for the created agents" + } + }, + "required": ["agent_ids", "group_id", "deployment_id"], + "description": "Response containing created agent IDs and associated metadata" + } + } + } + }, + "402": { + "description": "402", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "limit": { + "type": "number" + } + }, + "required": ["message", "limit"] + } + } + } + } + } + } + }, + "/v1/templates": { + "get": { + "description": "List all templates", + "summary": "List templates (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "offset", + "in": "query", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + { + "name": "exact", + "in": "query", + "description": "Whether to search for an exact name match", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "version", + "in": "query", + "description": "Specify the version you want to return, otherwise will return the latest version", + "schema": { + "type": "string" + } + }, + { + "name": "template_id", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "project_slug", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "project_id", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "sort_by", + "in": "query", + "schema": { + "type": "string", + "enum": ["updated_at", "created_at"] + } + } + ], + "operationId": "templates.listTemplates", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "templates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The exact name of the template" + }, + "id": { + "type": "string" + }, + "project_id": { + "type": "string" + }, + "project_slug": { + "type": "string" + }, + "latest_version": { + "type": "string", + "description": "The latest version of the template" + }, + "description": { + "type": "string" + }, + "template_deployment_slug": { + "type": "string", + "description": "The full name of the template, including version and project slug" + }, + "updated_at": { + "type": "string", + "description": "When the template was last updated" + } + }, + "required": [ + "name", + "id", + "project_id", + "project_slug", + "latest_version", + "template_deployment_slug", + "updated_at" + ] + } + }, + "has_next_page": { + "type": "boolean" + } + }, + "required": ["templates", "has_next_page"] + } + } + } + } + } + }, + "post": { + "description": "Creates a new template from an existing agent or agent file", + "summary": "Create template (Cloud-only)", + "tags": ["templates"], + "parameters": [], + "operationId": "templates.createTemplateNoProject", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["agent"] + }, + "agent_id": { + "type": "string", + "description": "The ID of the agent to use as a template, can be from any project" + }, + "name": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Optional custom name for the template. If not provided, a random name will be generated." + } + }, + "required": ["type", "agent_id"], + "summary": "From Agent", + "description": "Create a template from an existing agent" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["agent_file"] + }, + "agent_file": { + "type": "object", + "additionalProperties": { + "nullable": true + }, + "description": "The agent file to use as a template, this should be a JSON file exported from the platform" + }, + "name": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Optional custom name for the template. If not provided, a random name will be generated." + }, + "update_existing_tools": { + "type": "boolean", + "description": "If true, update existing custom tools source_code and json_schema (source_type cannot be changed)" + } + }, + "required": ["type", "agent_file"], + "summary": "From Agent File", + "description": "Create a template from an uploaded agent file" + } + ], + "summary": "Create template", + "description": "The type of template to create, currently only agent templates are supported" + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The exact name of the template" + }, + "id": { + "type": "string" + }, + "project_id": { + "type": "string" + }, + "project_slug": { + "type": "string" + }, + "latest_version": { + "type": "string", + "description": "The latest version of the template" + }, + "description": { + "type": "string" + }, + "template_deployment_slug": { + "type": "string", + "description": "The full name of the template, including version and project slug" + }, + "updated_at": { + "type": "string", + "description": "When the template was last updated" + } + }, + "required": [ + "name", + "id", + "project_id", + "project_slug", + "latest_version", + "template_deployment_slug", + "updated_at" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{template_name}/save": { + "post": { + "description": "Saves the current version of the template as a new version", + "summary": "Save template version (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "template_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template version, formatted as {template-name}, any version appended will be ignored" + } + ], + "operationId": "templates.saveTemplateVersionNoProject", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "preserve_environment_variables_on_migration": { + "type": "boolean", + "description": "If true, the environment variables will be preserved in the template version when migrating agents" + }, + "preserve_core_memories_on_migration": { + "type": "boolean", + "description": "If true, the core memories will be preserved in the template version when migrating agents" + }, + "preserve_sources_on_migration": { + "type": "boolean", + "description": "If true, existing agent folders/sources will be preserved and merged with template sources during migration. If false, agent sources will be replaced with template sources." + }, + "block_reconciliation_strategy": { + "type": "string", + "enum": ["reconcile-all", "preserve-deleted"], + "description": "Strategy for reconciling memory blocks during migration: \"reconcile-all\" deletes blocks not in the template, \"preserve-deleted\" keeps them. Defaults to \"preserve-deleted\"." + }, + "migrate_agents": { + "type": "boolean", + "description": "If true, existing agents attached to this template will be migrated to the new template version" + }, + "message": { + "type": "string", + "description": "A message to describe the changes made in this template version" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The exact name of the template" + }, + "id": { + "type": "string" + }, + "project_id": { + "type": "string" + }, + "project_slug": { + "type": "string" + }, + "latest_version": { + "type": "string", + "description": "The latest version of the template" + }, + "description": { + "type": "string" + }, + "template_deployment_slug": { + "type": "string", + "description": "The full name of the template, including version and project slug" + }, + "updated_at": { + "type": "string", + "description": "When the template was last updated" + } + }, + "required": [ + "name", + "id", + "project_id", + "project_slug", + "latest_version", + "template_deployment_slug", + "updated_at" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{project_id}/{template_name}": { + "post": { + "description": "Saves the current version of the template as a new version", + "summary": "Save template version (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + }, + { + "name": "template_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template version, formatted as {template-name}, any version appended will be ignored" + } + ], + "operationId": "templates.saveTemplateVersion", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "preserve_environment_variables_on_migration": { + "type": "boolean", + "description": "If true, the environment variables will be preserved in the template version when migrating agents" + }, + "preserve_core_memories_on_migration": { + "type": "boolean", + "description": "If true, the core memories will be preserved in the template version when migrating agents" + }, + "preserve_sources_on_migration": { + "type": "boolean", + "description": "If true, existing agent folders/sources will be preserved and merged with template sources during migration. If false, agent sources will be replaced with template sources." + }, + "block_reconciliation_strategy": { + "type": "string", + "enum": ["reconcile-all", "preserve-deleted"], + "description": "Strategy for reconciling memory blocks during migration: \"reconcile-all\" deletes blocks not in the template, \"preserve-deleted\" keeps them. Defaults to \"preserve-deleted\"." + }, + "migrate_agents": { + "type": "boolean", + "description": "If true, existing agents attached to this template will be migrated to the new template version" + }, + "message": { + "type": "string", + "description": "A message to describe the changes made in this template version" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The exact name of the template" + }, + "id": { + "type": "string" + }, + "project_id": { + "type": "string" + }, + "project_slug": { + "type": "string" + }, + "latest_version": { + "type": "string", + "description": "The latest version of the template" + }, + "description": { + "type": "string" + }, + "template_deployment_slug": { + "type": "string", + "description": "The full name of the template, including version and project slug" + }, + "updated_at": { + "type": "string", + "description": "When the template was last updated" + } + }, + "required": [ + "name", + "id", + "project_id", + "project_slug", + "latest_version", + "template_deployment_slug", + "updated_at" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + }, + "delete": { + "description": "Deletes all versions of a template with the specified name", + "summary": "Delete template (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + }, + { + "name": "template_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template name (without version)" + } + ], + "operationId": "templates.deleteTemplate", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{project_id}/{template_version}/snapshot": { + "get": { + "description": "Get a snapshot of the template version, this will return the template state at a specific version", + "summary": "Get template snapshot (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + }, + { + "name": "template_version", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template version, formatted as {template-name}:{version-number} or {template-name}:latest" + } + ], + "operationId": "templates.getTemplateSnapshot", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "systemPrompt": { + "type": "string" + }, + "toolIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "sourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "memoryVariables": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "defaultValue": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string" + } + }, + "required": ["key", "type"] + } + } + }, + "required": ["version", "data"], + "nullable": true + }, + "toolVariables": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "defaultValue": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string" + } + }, + "required": ["key", "type"] + } + } + }, + "required": ["version", "data"], + "nullable": true + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "identityIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "toolRules": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "tool_name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["constrain_child_tools"] + }, + "prompt_template": { + "type": "string", + "nullable": true + }, + "children": { + "type": "array", + "items": { + "type": "string" + } + }, + "child_arg_nodes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "args": { + "type": "object", + "additionalProperties": {}, + "nullable": true + } + }, + "required": ["name"] + }, + "nullable": true + } + }, + "required": ["tool_name", "children"] + }, + { + "type": "object", + "properties": { + "tool_name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["run_first"] + }, + "prompt_template": { + "type": "string", + "nullable": true + }, + "args": { + "type": "object", + "additionalProperties": {}, + "nullable": true + } + }, + "required": ["tool_name"] + }, + { + "type": "object", + "properties": { + "tool_name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["exit_loop"] + }, + "prompt_template": { + "type": "string", + "nullable": true + } + }, + "required": ["tool_name"] + }, + { + "type": "object", + "properties": { + "tool_name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["conditional"] + }, + "prompt_template": { + "type": "string", + "nullable": true + }, + "default_child": { + "type": "string", + "nullable": true + }, + "child_output_mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "require_output_mapping": { + "type": "boolean" + } + }, + "required": [ + "tool_name", + "child_output_mapping" + ] + }, + { + "type": "object", + "properties": { + "tool_name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["continue_loop"] + }, + "prompt_template": { + "type": "string", + "nullable": true + } + }, + "required": ["tool_name"] + }, + { + "type": "object", + "properties": { + "tool_name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["required_before_exit"] + }, + "prompt_template": { + "type": "string", + "nullable": true + } + }, + "required": ["tool_name"] + }, + { + "type": "object", + "properties": { + "tool_name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["max_count_per_step"] + }, + "prompt_template": { + "type": "string", + "nullable": true + }, + "max_count_limit": { + "type": "number" + } + }, + "required": ["tool_name", "max_count_limit"] + }, + { + "type": "object", + "properties": { + "tool_name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["parent_last_tool"] + }, + "prompt_template": { + "type": "string", + "nullable": true + }, + "children": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["tool_name", "children"] + }, + { + "type": "object", + "properties": { + "tool_name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["requires_approval"] + }, + "prompt_template": { + "type": "string", + "nullable": true + } + }, + "required": ["tool_name"] + } + ] + }, + "nullable": true + }, + "agentType": { + "type": "string", + "enum": [ + "letta_v1_agent", + "memgpt_agent", + "memgpt_v2_agent", + "react_agent", + "workflow_agent", + "split_thread_agent", + "sleeptime_agent", + "voice_convo_agent", + "voice_sleeptime_agent" + ] + }, + "properties": { + "type": "object", + "properties": { + "enable_reasoner": { + "type": "boolean", + "nullable": true + }, + "put_inner_thoughts_in_kwargs": { + "type": "boolean", + "nullable": true + }, + "context_window_limit": { + "type": "number", + "nullable": true + }, + "max_tokens": { + "type": "number", + "nullable": true + }, + "max_reasoning_tokens": { + "type": "number", + "nullable": true + }, + "max_files_open": { + "type": "number", + "nullable": true + }, + "message_buffer_autoclear": { + "type": "boolean", + "nullable": true + }, + "verbosity_level": { + "type": "string", + "enum": ["low", "medium", "high"], + "nullable": true + }, + "reasoning_effort": { + "type": "string", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "nullable": true + }, + "per_file_view_window_char_limit": { + "type": "number", + "nullable": true + }, + "parallel_tool_calls": { + "type": "boolean", + "nullable": true + }, + "temperature": { + "type": "number", + "nullable": true + } + }, + "required": [ + "enable_reasoner", + "put_inner_thoughts_in_kwargs", + "context_window_limit", + "max_tokens", + "max_reasoning_tokens", + "max_files_open", + "message_buffer_autoclear", + "verbosity_level", + "reasoning_effort", + "per_file_view_window_char_limit", + "parallel_tool_calls", + "temperature" + ], + "nullable": true + }, + "entityId": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "model", + "systemPrompt", + "toolIds", + "sourceIds", + "memoryVariables", + "toolVariables", + "tags", + "identityIds", + "toolRules", + "agentType", + "properties", + "entityId", + "name" + ] + } + }, + "blocks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "entityId": { + "type": "string" + }, + "label": { + "type": "string" + }, + "value": { + "type": "string" + }, + "limit": { + "type": "number" + }, + "description": { + "type": "string" + }, + "preserveOnMigration": { + "type": "boolean", + "nullable": true + }, + "readOnly": { + "type": "boolean" + } + }, + "required": [ + "entityId", + "label", + "value", + "limit", + "description", + "preserveOnMigration", + "readOnly" + ] + } + }, + "relationships": { + "type": "array", + "items": { + "type": "object", + "properties": { + "agentEntityId": { + "type": "string" + }, + "blockEntityId": { + "type": "string" + } + }, + "required": ["agentEntityId", "blockEntityId"] + } + }, + "configuration": { + "type": "object", + "properties": { + "managerAgentEntityId": { + "type": "string" + }, + "managerType": { + "type": "string" + }, + "terminationToken": { + "type": "string" + }, + "maxTurns": { + "type": "number" + }, + "sleeptimeAgentFrequency": { + "type": "number" + }, + "maxMessageBufferLength": { + "type": "number" + }, + "minMessageBufferLength": { + "type": "number" + } + } + }, + "type": { + "type": "string", + "enum": [ + "classic", + "cluster", + "sleeptime", + "round_robin", + "supervisor", + "dynamic", + "voice_sleeptime" + ] + }, + "version": { + "type": "string" + } + }, + "required": [ + "agents", + "blocks", + "relationships", + "configuration", + "type", + "version" + ] + } + } + } + } + } + }, + "put": { + "description": "Updates the current working version of a template from a snapshot", + "summary": "Set current template from snapshot (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + }, + { + "name": "template_version", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template name with :dev version (e.g., my-template:dev)" + } + ], + "operationId": "templates.setCurrentTemplateFromSnapshot", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "nullable": true, + "description": "The template snapshot to set as the current version" + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{project_id}/{template_version}/fork": { + "post": { + "description": "Forks a template version into a new template", + "summary": "Fork template (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + }, + { + "name": "template_version", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template version, formatted as {template-name}:{version-number} or {template-name}:latest" + } + ], + "operationId": "templates.forkTemplate", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Optional custom name for the forked template. If not provided, a random name will be generated." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The exact name of the template" + }, + "id": { + "type": "string" + }, + "project_id": { + "type": "string" + }, + "project_slug": { + "type": "string" + }, + "latest_version": { + "type": "string", + "description": "The latest version of the template" + }, + "description": { + "type": "string" + }, + "template_deployment_slug": { + "type": "string", + "description": "The full name of the template, including version and project slug" + }, + "updated_at": { + "type": "string", + "description": "When the template was last updated" + } + }, + "required": [ + "name", + "id", + "project_id", + "project_slug", + "latest_version", + "template_deployment_slug", + "updated_at" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{project_id}": { + "post": { + "description": "Creates a new template from an existing agent or agent file", + "summary": "Create template (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + } + ], + "operationId": "templates.createTemplate", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["agent"] + }, + "agent_id": { + "type": "string", + "description": "The ID of the agent to use as a template, can be from any project" + }, + "name": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Optional custom name for the template. If not provided, a random name will be generated." + } + }, + "required": ["type", "agent_id"], + "summary": "From Agent", + "description": "Create a template from an existing agent" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["agent_file"] + }, + "agent_file": { + "type": "object", + "additionalProperties": { + "nullable": true + }, + "description": "The agent file to use as a template, this should be a JSON file exported from the platform" + }, + "name": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Optional custom name for the template. If not provided, a random name will be generated." + }, + "update_existing_tools": { + "type": "boolean", + "description": "If true, update existing custom tools source_code and json_schema (source_type cannot be changed)" + } + }, + "required": ["type", "agent_file"], + "summary": "From Agent File", + "description": "Create a template from an uploaded agent file" + } + ], + "summary": "Create template", + "description": "The type of template to create, currently only agent templates are supported" + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The exact name of the template" + }, + "id": { + "type": "string" + }, + "project_id": { + "type": "string" + }, + "project_slug": { + "type": "string" + }, + "latest_version": { + "type": "string", + "description": "The latest version of the template" + }, + "description": { + "type": "string" + }, + "template_deployment_slug": { + "type": "string", + "description": "The full name of the template, including version and project slug" + }, + "updated_at": { + "type": "string", + "description": "When the template was last updated" + } + }, + "required": [ + "name", + "id", + "project_id", + "project_slug", + "latest_version", + "template_deployment_slug", + "updated_at" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{template_name}": { + "delete": { + "description": "Deletes all versions of a template with the specified name", + "summary": "Delete template (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "template_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template name (without version)" + } + ], + "operationId": "templates.deleteTemplateNoProject", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + }, + "patch": { + "description": "Updates the current working version of a template from an agent file", + "summary": "Update current template from agent file (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "template_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template name (without version)" + } + ], + "operationId": "templates.updateCurrentTemplateFromAgentFileNoProject", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent_file_json": { + "type": "object", + "additionalProperties": { + "nullable": true + }, + "description": "The agent file to update the current template version from" + }, + "update_existing_tools": { + "default": false, + "type": "boolean", + "description": "If true, update existing custom tools source_code and json_schema (source_type cannot be changed)" + }, + "save_existing_changes": { + "default": false, + "type": "boolean", + "description": "If true, Letta will automatically save any changes as a version before updating the template" + } + }, + "required": ["agent_file_json"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{project_id}/{template_name}/name": { + "patch": { + "description": "Renames all versions of a template with the specified name. Versions are automatically stripped from the current template name if accidentally included.", + "summary": "Rename template (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + }, + { + "name": "template_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The current template name (version will be automatically stripped if included)" + } + ], + "operationId": "templates.renameTemplate", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "new_name": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "The new name for the template" + } + }, + "required": ["new_name"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{project_id}/{template_name}/description": { + "patch": { + "description": "Updates the description for all versions of a template with the specified name. Versions are automatically stripped from the current template name if accidentally included.", + "summary": "Update template description (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + }, + { + "name": "template_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template name (version will be automatically stripped if included)" + } + ], + "operationId": "templates.updateTemplateDescription", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "The new description for the template" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{project_id}/{name}/versions": { + "get": { + "description": "List all versions of a specific template", + "summary": "List template versions (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template name (without version)" + }, + { + "name": "offset", + "in": "query", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "operationId": "templates.listTemplateVersions", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "versions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "The version number" + }, + "created_at": { + "type": "string", + "description": "When the version was created" + }, + "message": { + "type": "string", + "description": "Version description message" + }, + "is_latest": { + "type": "boolean", + "description": "Whether this is the latest version" + } + }, + "required": ["version", "created_at", "is_latest"] + } + }, + "has_next_page": { + "type": "boolean" + }, + "total_count": { + "type": "number" + } + }, + "required": ["versions", "has_next_page", "total_count"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{project_id}/{template_name}/deployments/{deployment_id}/migrate": { + "post": { + "description": "Migrates a deployment to a specific template version", + "summary": "Migrate deployment to template version (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + }, + { + "name": "template_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template name (without version)" + }, + { + "name": "deployment_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The deployment ID to migrate" + } + ], + "operationId": "templates.migrateDeployment", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "The target template version to migrate to" + }, + "preserve_tool_variables": { + "type": "boolean", + "description": "Whether to preserve existing tool variables during migration" + }, + "preserve_core_memories": { + "type": "boolean", + "description": "Whether to preserve existing core memories during migration" + }, + "preserve_sources": { + "type": "boolean", + "description": "If true, existing agent sources will be preserved and merged with template sources during migration. If false, agent sources will be replaced with template sources." + }, + "memory_variables": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Additional memory variables to apply during migration" + } + }, + "required": ["version"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{template_name}/rollback": { + "post": { + "description": "Rollback the current working version of a template to a previous saved version. If the current version has unsaved changes, they will be automatically saved as a new version before rollback.", + "summary": "Rollback template to previous version (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "template_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template name (without version)" + } + ], + "operationId": "templates.rollbackTemplateNoProject", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "The target version to rollback to (e.g., \"1\", \"2\", \"latest\"). Cannot be \"current\" or \"dev\"." + } + }, + "required": ["version"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{project_id}/{template_name}/rollback": { + "post": { + "description": "Rollback the current working version of a template to a previous saved version. If the current version has unsaved changes, they will be automatically saved as a new version before rollback.", + "summary": "Rollback template to previous version (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + }, + { + "name": "template_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template name (without version)" + } + ], + "operationId": "templates.rollbackTemplate", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "The target version to rollback to (e.g., \"1\", \"2\", \"latest\"). Cannot be \"current\" or \"dev\"." + } + }, + "required": ["version"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/templates/{project_id}/{template_name}/agent-file": { + "put": { + "description": "Updates the current working version of a template from an agent file", + "summary": "Update current template from agent file (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The project id" + }, + { + "name": "template_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template name (without version)" + } + ], + "operationId": "templates.updateCurrentTemplateFromAgentFile", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent_file_json": { + "type": "object", + "additionalProperties": { + "nullable": true + }, + "description": "The agent file to update the current template version from" + }, + "update_existing_tools": { + "default": false, + "type": "boolean", + "description": "If true, update existing custom tools source_code and json_schema (source_type cannot be changed)" + }, + "save_existing_changes": { + "default": false, + "type": "boolean", + "description": "If true, Letta will automatically save any changes as a version before updating the template" + } + }, + "required": ["agent_file_json"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/legacy-templates/{templateId}/migrates": { + "post": { + "description": "Migrates a template from a legacy project to the default project. Only works if the template is currently in a legacy project.", + "summary": "Migrate template from legacy project (Cloud-only)", + "tags": ["templates"], + "parameters": [ + { + "name": "templateId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The template ID" + } + ], + "operationId": "templates.legacyMigration", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/client-side-access-tokens": { + "post": { + "description": "Create a new client side access token with the specified configuration.", + "summary": "Create token (Cloud-only)", + "tags": ["clientSideAccessTokens"], + "parameters": [], + "operationId": "clientSideAccessTokens.createClientSideAccessToken", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "policy": { + "type": "array", + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["agent"] + }, + "id": { + "type": "string" + }, + "access": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "read_messages", + "write_messages", + "read_agent", + "write_agent" + ] + } + } + }, + "required": ["type", "id", "access"] + } + ] + } + }, + "hostname": { + "type": "string", + "format": "uri", + "pattern": "^(http|https):\\/\\/", + "description": "The hostname of the client side application. Please specify the full URL including the protocol (http or https)." + }, + "expires_at": { + "type": "string", + "description": "The expiration date of the token. If not provided, the token will expire in 5 minutes" + } + }, + "required": ["policy", "hostname"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "policy": { + "type": "object", + "properties": { + "version": { + "type": "string", + "enum": ["1"] + }, + "data": { + "type": "array", + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["agent"] + }, + "id": { + "type": "string" + }, + "access": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "read_messages", + "write_messages", + "read_agent", + "write_agent" + ] + } + } + }, + "required": ["type", "id", "access"] + } + ] + } + } + }, + "required": ["version", "data"] + }, + "token": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "expiresAt": { + "type": "string" + } + }, + "required": ["policy", "token", "hostname", "expiresAt"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + }, + "get": { + "description": "List all client side access tokens for the current account. This is only available for cloud users.", + "summary": "List tokens (Cloud-only)", + "tags": ["clientSideAccessTokens"], + "parameters": [ + { + "name": "agentId", + "in": "query", + "description": "The agent ID to filter tokens by. If provided, only tokens for this agent will be returned.", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", + "description": "The offset for pagination. Defaults to 0.", + "schema": { + "default": 0, + "type": "number" + } + }, + { + "name": "limit", + "in": "query", + "description": "The number of tokens to return per page. Defaults to 10.", + "schema": { + "default": 10, + "type": "number" + } + } + ], + "operationId": "clientSideAccessTokens.listClientSideAccessTokens", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tokens": { + "type": "array", + "items": { + "type": "object", + "properties": { + "policy": { + "type": "object", + "properties": { + "version": { + "type": "string", + "enum": ["1"] + }, + "data": { + "type": "array", + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["agent"] + }, + "id": { + "type": "string" + }, + "access": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "read_messages", + "write_messages", + "read_agent", + "write_agent" + ] + } + } + }, + "required": ["type", "id", "access"] + } + ] + } + } + }, + "required": ["version", "data"] + }, + "token": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "expiresAt": { + "type": "string" + } + }, + "required": ["policy", "token", "hostname", "expiresAt"] + } + }, + "hasNextPage": { + "type": "boolean" + } + }, + "required": ["tokens", "hasNextPage"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/client-side-access-tokens/{token}": { + "delete": { + "description": "Delete a client side access token.", + "summary": "Delete token (Cloud-only)", + "tags": ["clientSideAccessTokens"], + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The access token to delete" + } + ], + "operationId": "clientSideAccessTokens.deleteClientSideAccessToken", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": {} + } + } + }, + "responses": { + "204": { + "description": "204", + "content": { + "application/json": { + "schema": {} + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/projects": { + "get": { + "description": "List all projects", + "summary": "List Projects (Cloud-only)", + "tags": ["projects"], + "parameters": [ + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "operationId": "projects.listProjects", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "projects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": ["name", "slug", "id"] + } + }, + "hasNextPage": { + "type": "boolean" + } + }, + "required": ["projects", "hasNextPage"] + } + } + } + } + } + }, + "post": { + "description": "Create a new project", + "summary": "Create Project (Cloud-only)", + "tags": ["projects"], + "parameters": [], + "operationId": "projects.createProject", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 3, + "maxLength": 50 + } + }, + "required": ["name"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": ["name", "slug", "id"] + } + } + } + } + } + } + }, + "/v1/projects/{projectId}": { + "delete": { + "description": "Delete a project by ID", + "summary": "Delete Project (Cloud-only)", + "tags": ["projects"], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "operationId": "projects.deleteProject", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "nullable": true + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + } + } + } + }, + "/v1/metadata/balance": { + "get": { + "description": "Retrieve the current usage balances for the organization.", + "summary": "Retrieve current organization balance", + "tags": ["metadata"], + "parameters": [], + "operationId": "metadata.retrieveCurrentBalances", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "total_balance": { + "type": "number" + }, + "monthly_credit_balance": { + "type": "number" + }, + "purchased_credit_balance": { + "type": "number" + }, + "billing_tier": { + "type": "string" + } + }, + "required": [ + "total_balance", + "monthly_credit_balance", + "purchased_credit_balance", + "billing_tier" + ] + } + } + } + } + } + } + }, + "/v1/metadata/feedback": { + "post": { + "description": "Send feedback from users to improve our services.", + "summary": "Send user feedback", + "tags": ["metadata"], + "parameters": [], + "operationId": "metadata.sendFeedback", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "minLength": 1, + "maxLength": 10000 + }, + "feature": { + "default": "letta-code", + "type": "string", + "enum": ["letta-code", "sdk"] + }, + "agent_id": { + "type": "string" + }, + "session_id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "settings": { + "type": "string" + }, + "local_time": { + "type": "string" + }, + "device_type": { + "type": "string" + }, + "cwd": { + "type": "string" + }, + "total_api_ms": { + "type": "number" + }, + "total_wall_ms": { + "type": "number" + }, + "step_count": { + "type": "number" + }, + "prompt_tokens": { + "type": "number" + }, + "completion_tokens": { + "type": "number" + }, + "total_tokens": { + "type": "number" + }, + "cached_input_tokens": { + "type": "number" + }, + "cache_write_tokens": { + "type": "number" + }, + "reasoning_tokens": { + "type": "number" + }, + "context_tokens": { + "type": "number" + }, + "agent_name": { + "type": "string" + }, + "agent_description": { + "type": "string" + }, + "model": { + "type": "string" + }, + "billing_tier": { + "type": "string" + }, + "recent_chunks": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + } + }, + "debug_log_tail": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + } + } + } + }, + "/v1/metadata/telemetry": { + "post": { + "description": "Send telemetry events for usage tracking and analysis.", + "summary": "Send telemetry event", + "tags": ["metadata"], + "parameters": [], + "operationId": "metadata.sendTelemetry", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "service": { + "type": "string", + "enum": ["letta-code"] + }, + "server_version": { + "type": "string" + }, + "events": { + "type": "array", + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["session_start"] + }, + "timestamp": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "surface": { + "type": "string" + }, + "startup_command": { + "type": "string" + }, + "version": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "node_version": { + "type": "string" + } + }, + "required": ["session_id", "startup_command"] + } + }, + "required": ["type", "timestamp", "data"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["session_end"] + }, + "timestamp": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "surface": { + "type": "string" + }, + "duration": { + "type": "number" + }, + "message_count": { + "type": "number" + }, + "tool_call_count": { + "type": "number" + }, + "exit_reason": { + "type": "string" + }, + "total_api_ms": { + "type": "number" + }, + "total_wall_ms": { + "type": "number" + }, + "prompt_tokens": { + "type": "number" + }, + "completion_tokens": { + "type": "number" + }, + "total_tokens": { + "type": "number" + }, + "cached_tokens": { + "type": "number" + }, + "reasoning_tokens": { + "type": "number" + }, + "step_count": { + "type": "number" + } + }, + "required": [ + "session_id", + "duration", + "message_count", + "tool_call_count" + ] + } + }, + "required": ["type", "timestamp", "data"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["tool_usage"] + }, + "timestamp": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "surface": { + "type": "string" + }, + "tool_name": { + "type": "string" + }, + "success": { + "type": "boolean" + }, + "duration": { + "type": "number" + }, + "response_length": { + "type": "number" + }, + "error_type": { + "type": "string" + }, + "stderr": { + "type": "string" + } + }, + "required": [ + "session_id", + "tool_name", + "success", + "duration" + ] + } + }, + "required": ["type", "timestamp", "data"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["error"] + }, + "timestamp": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "run_id": { + "type": "string" + }, + "error_type": { + "type": "string" + }, + "error_message": { + "type": "string" + }, + "context": { + "type": "string" + }, + "http_status": { + "type": "number" + }, + "model_id": { + "type": "string" + }, + "surface": { + "type": "string" + }, + "recent_chunks": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + } + }, + "debug_log_tail": { + "type": "string" + } + }, + "required": [ + "session_id", + "error_type", + "error_message" + ] + } + }, + "required": ["type", "timestamp", "data"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["user_input"] + }, + "timestamp": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "surface": { + "type": "string" + }, + "input_length": { + "type": "number" + }, + "is_command": { + "type": "boolean" + }, + "command_name": { + "type": "string" + }, + "message_type": { + "type": "string" + }, + "model_id": { + "type": "string" + } + }, + "required": [ + "session_id", + "input_length", + "is_command", + "message_type", + "model_id" + ] + } + }, + "required": ["type", "timestamp", "data"] + } + ] + }, + "minItems": 1 + } + }, + "required": ["service", "events"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + } + } + } + }, + "/v1/metadata/status": { + "get": { + "summary": "Gets your Letta Cloud status", + "tags": ["metadata"], + "parameters": [], + "operationId": "metadata.getStatus", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "current_project_id": { + "type": "string", + "nullable": true + } + }, + "required": ["current_project_id"] + } + } + } + } + } + } + }, + "/v1/metadata/user": { + "get": { + "description": "Retrieve information about the current authenticated user including email, name, organization, and current project.", + "summary": "Get current user information", + "tags": ["metadata"], + "parameters": [], + "operationId": "metadata.getUser", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organization_name": { + "type": "string" + }, + "organization_id": { + "type": "string" + }, + "current_project_name": { + "type": "string", + "nullable": true + }, + "current_project_id": { + "type": "string", + "nullable": true + }, + "billing_tier": { + "type": "string" + }, + "remaining_credits": { + "type": "number" + } + }, + "required": [ + "id", + "email", + "name", + "organization_name", + "organization_id", + "current_project_name", + "current_project_id", + "billing_tier", + "remaining_credits" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/schedule": { + "post": { + "description": "Schedule a message to be sent by the agent at a specified time or on a recurring basis.", + "summary": "Schedule Agent Message", + "tags": ["scheduledMessages"], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "scheduledMessages.scheduleAgentMessage", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "content": { + "oneOf": [ + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "signature": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string", + "enum": ["text"] + } + }, + "required": ["text"] + }, + { + "type": "object", + "properties": { + "source": { + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "media_type": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["base64"] + } + }, + "required": ["data", "media_type"] + }, + "type": { + "type": "string", + "enum": ["image"] + } + }, + "required": ["source", "type"] + } + ] + } + }, + { + "type": "string" + } + ] + }, + "role": { + "type": "string", + "enum": ["user", "assistant", "system"] + }, + "name": { + "type": "string" + }, + "otid": { + "type": "string" + }, + "sender_id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["message"] + } + }, + "required": ["content", "role"] + } + }, + "max_steps": { + "type": "number" + }, + "callback_url": { + "type": "string", + "format": "uri" + }, + "include_return_message_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "system_message", + "user_message", + "assistant_message", + "reasoning_message", + "hidden_reasoning_message", + "tool_call_message", + "tool_return_message", + "approval_request_message", + "approval_response_message" + ] + } + }, + "schedule": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["one-time"] + }, + "scheduled_at": { + "type": "number" + } + }, + "required": ["scheduled_at"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["recurring"] + }, + "cron_expression": { + "type": "string" + } + }, + "required": ["type", "cron_expression"] + } + ] + } + }, + "required": ["messages", "schedule"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "next_scheduled_at": { + "type": "string" + } + }, + "required": ["id"] + } + } + } + } + } + }, + "get": { + "description": "List all scheduled messages for a specific agent.", + "summary": "List Scheduled Agent Messages", + "tags": ["scheduledMessages"], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "after", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "operationId": "scheduledMessages.listScheduledMessages", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "scheduled_messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "message": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "content": { + "oneOf": [ + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "signature": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string", + "enum": ["text"] + } + }, + "required": ["text"] + }, + { + "type": "object", + "properties": { + "source": { + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "media_type": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["base64"] + } + }, + "required": [ + "data", + "media_type" + ] + }, + "type": { + "type": "string", + "enum": ["image"] + } + }, + "required": ["source", "type"] + } + ] + } + }, + { + "type": "string" + } + ] + }, + "role": { + "type": "string", + "enum": ["user", "assistant", "system"] + }, + "name": { + "type": "string" + }, + "otid": { + "type": "string" + }, + "sender_id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["message"] + } + }, + "required": ["content", "role"] + } + }, + "max_steps": { + "type": "number" + }, + "callback_url": { + "type": "string", + "format": "uri" + }, + "include_return_message_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "system_message", + "user_message", + "assistant_message", + "reasoning_message", + "hidden_reasoning_message", + "tool_call_message", + "tool_return_message", + "approval_request_message", + "approval_response_message" + ] + } + } + }, + "required": ["messages"] + }, + "schedule": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["one-time"] + }, + "scheduled_at": { + "type": "number" + } + }, + "required": ["scheduled_at"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["recurring"] + }, + "cron_expression": { + "type": "string" + } + }, + "required": ["type", "cron_expression"] + } + ] + }, + "next_scheduled_time": { + "type": "string", + "nullable": true + } + }, + "required": [ + "id", + "agent_id", + "message", + "schedule", + "next_scheduled_time" + ] + } + }, + "has_next_page": { + "type": "boolean" + } + }, + "required": ["scheduled_messages", "has_next_page"] + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/schedule/{scheduled_message_id}": { + "delete": { + "description": "Delete a scheduled message by its ID for a specific agent.", + "summary": "Delete Scheduled Agent Message", + "tags": ["scheduledMessages"], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "scheduled_message_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "scheduledMessages.deleteScheduledMessage", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {}, + "nullable": true + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [true] + } + }, + "required": ["success"] + } + } + } + } + } + }, + "get": { + "description": "Retrieve a scheduled message by its ID for a specific agent.", + "summary": "Retrieve Scheduled Agent Message", + "tags": ["scheduledMessages"], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "scheduled_message_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "scheduledMessages.retrieveScheduledMessage", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "message": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "content": { + "oneOf": [ + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "signature": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string", + "enum": ["text"] + } + }, + "required": ["text"] + }, + { + "type": "object", + "properties": { + "source": { + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "media_type": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["base64"] + } + }, + "required": ["data", "media_type"] + }, + "type": { + "type": "string", + "enum": ["image"] + } + }, + "required": ["source", "type"] + } + ] + } + }, + { + "type": "string" + } + ] + }, + "role": { + "type": "string", + "enum": ["user", "assistant", "system"] + }, + "name": { + "type": "string" + }, + "otid": { + "type": "string" + }, + "sender_id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["message"] + } + }, + "required": ["content", "role"] + } + }, + "max_steps": { + "type": "number" + }, + "callback_url": { + "type": "string", + "format": "uri" + }, + "include_return_message_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "system_message", + "user_message", + "assistant_message", + "reasoning_message", + "hidden_reasoning_message", + "tool_call_message", + "tool_return_message", + "approval_request_message", + "approval_response_message" + ] + } + } + }, + "required": ["messages"] + }, + "schedule": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["one-time"] + }, + "scheduled_at": { + "type": "number" + } + }, + "required": ["scheduled_at"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["recurring"] + }, + "cron_expression": { + "type": "string" + } + }, + "required": ["type", "cron_expression"] + } + ] + }, + "next_scheduled_time": { + "type": "string", + "nullable": true + } + }, + "required": [ + "id", + "agent_id", + "message", + "schedule", + "next_scheduled_time" + ] + } + } + } + } + } + } + }, + "/v1/feeds": { + "post": { + "description": "Create a new feed in a project", + "summary": "Create Feed", + "tags": ["feeds"], + "parameters": [], + "operationId": "feeds.createFeed", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "project_id": { + "type": "string" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 500 + } + }, + "required": ["project_id", "name"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "project_id": { + "type": "string" + }, + "organization_id": { + "type": "string" + }, + "created_by_id": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "description", + "project_id", + "organization_id", + "created_by_id", + "created_at", + "updated_at" + ] + } + } + } + } + } + }, + "get": { + "description": "List all feeds with optional filters and pagination", + "summary": "List Feeds", + "tags": ["feeds"], + "parameters": [ + { + "name": "project_id", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + } + ], + "operationId": "feeds.listFeeds", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "feeds": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "project_id": { + "type": "string" + }, + "organization_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "subscriptions_count": { + "type": "number" + } + }, + "required": [ + "id", + "name", + "description", + "project_id", + "organization_id", + "created_at", + "updated_at", + "subscriptions_count" + ] + } + }, + "has_next_page": { + "type": "boolean" + } + }, + "required": ["feeds", "has_next_page"] + } + } + } + } + } + } + }, + "/v1/feeds/{feed_id}": { + "get": { + "description": "Retrieve feed details by ID", + "summary": "Get Feed", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.getFeed", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "project_id": { + "type": "string" + }, + "organization_id": { + "type": "string" + }, + "created_by_id": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "subscriptions_count": { + "type": "number" + }, + "messages_count": { + "type": "number" + } + }, + "required": [ + "id", + "name", + "description", + "project_id", + "organization_id", + "created_by_id", + "created_at", + "updated_at", + "subscriptions_count" + ] + } + } + } + } + } + }, + "delete": { + "description": "Soft delete a feed and clean up its sequence", + "summary": "Delete Feed", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.deleteFeed", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {}, + "nullable": true + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + } + } + } + }, + "/v1/feeds/{feed_id}/messages": { + "post": { + "description": "Batch insert messages into a feed (up to 10,000 per request)", + "summary": "Publish Messages", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.publishMessages", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + }, + "required": ["content"] + }, + "minItems": 1, + "maxItems": 10000 + } + }, + "required": ["messages"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "inserted_count": { + "type": "number" + } + }, + "required": ["inserted_count"] + } + } + } + } + } + }, + "get": { + "description": "List messages from a feed (for debugging/inspection)", + "summary": "List Feed Messages", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "after_sequence", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.listMessages", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "feed_id": { + "type": "string" + }, + "sequence": { + "type": "number" + }, + "content_preview": { + "type": "string" + }, + "is_truncated": { + "type": "boolean" + }, + "content_size_bytes": { + "type": "number" + }, + "expires_at": { + "type": "string" + }, + "created_at": { + "type": "string" + } + }, + "required": [ + "id", + "feed_id", + "sequence", + "content_preview", + "is_truncated", + "content_size_bytes", + "expires_at", + "created_at" + ] + } + }, + "has_next_page": { + "type": "boolean" + }, + "next_cursor": { + "type": "number", + "nullable": true + } + }, + "required": ["messages", "has_next_page", "next_cursor"] + } + } + } + } + } + } + }, + "/v1/feeds/{feed_id}/messages/{message_id}": { + "get": { + "description": "Get full content of a feed message", + "summary": "Get Feed Message", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "message_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.getMessage", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "feed_id": { + "type": "string" + }, + "sequence": { + "type": "number" + }, + "content": { + "type": "string" + }, + "content_size_bytes": { + "type": "number" + }, + "expires_at": { + "type": "string" + }, + "created_at": { + "type": "string" + } + }, + "required": [ + "id", + "feed_id", + "sequence", + "content", + "content_size_bytes", + "expires_at", + "created_at" + ] + } + }, + "required": ["message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/feeds/{feed_id}/subscribe": { + "post": { + "description": "Subscribe an agent to a feed with polling configuration", + "summary": "Subscribe Agent to Feed", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.subscribeAgent", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent_id": { + "type": "string" + }, + "cron_schedule": { + "type": "string" + }, + "prompt_template": { + "type": "string" + } + }, + "required": ["agent_id", "cron_schedule"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "feed_id": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "agent_name": { + "type": "string", + "nullable": true + }, + "cron_schedule": { + "type": "string" + }, + "merge_strategy": { + "type": "string", + "enum": ["unique-messages", "combine-into-single-message"] + }, + "prompt_template": { + "type": "string", + "nullable": true + }, + "next_scheduled_at": { + "type": "string" + }, + "last_consumed_sequence": { + "type": "number" + }, + "last_consumed_at": { + "type": "string", + "nullable": true + }, + "disabled_at": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string" + } + }, + "required": [ + "id", + "feed_id", + "agent_id", + "agent_name", + "cron_schedule", + "merge_strategy", + "prompt_template", + "next_scheduled_at", + "last_consumed_sequence", + "last_consumed_at", + "disabled_at", + "created_at" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": [ + "agentAlreadySubscribed", + "agentNotInProject", + "invalidCronExpression" + ] + } + }, + "required": ["message", "errorCode"] + } + } + } + } + } + } + }, + "/v1/feeds/{feed_id}/subscriptions/{subscription_id}": { + "patch": { + "description": "Update subscription configuration (cron schedule, enable/disable)", + "summary": "Update Subscription", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "subscription_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.updateSubscription", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cron_schedule": { + "type": "string" + }, + "prompt_template": { + "type": "string" + }, + "disabled": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "feed_id": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "agent_name": { + "type": "string", + "nullable": true + }, + "cron_schedule": { + "type": "string" + }, + "merge_strategy": { + "type": "string", + "enum": ["unique-messages", "combine-into-single-message"] + }, + "prompt_template": { + "type": "string", + "nullable": true + }, + "next_scheduled_at": { + "type": "string" + }, + "last_consumed_sequence": { + "type": "number" + }, + "last_consumed_at": { + "type": "string", + "nullable": true + }, + "disabled_at": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "feed_id", + "agent_id", + "agent_name", + "cron_schedule", + "merge_strategy", + "prompt_template", + "next_scheduled_at", + "last_consumed_sequence", + "last_consumed_at", + "disabled_at", + "created_at", + "updated_at" + ] + } + } + } + } + } + }, + "delete": { + "description": "Remove agent subscription from a feed (by subscription_id)", + "summary": "Delete Subscription", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "subscription_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.deleteSubscription", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {}, + "nullable": true + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + } + } + } + }, + "/v1/feeds/{feed_id}/unsubscribe": { + "post": { + "description": "Remove agent subscription from a feed (by agent_id)", + "summary": "Unsubscribe Agent from Feed", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.unsubscribeAgent", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent_id": { + "type": "string" + } + }, + "required": ["agent_id"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + } + } + } + }, + "/v1/feeds/{feed_id}/subscriptions/{subscription_id}/trigger": { + "post": { + "description": "Immediately trigger a subscription to process pending messages", + "summary": "Trigger Subscription", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "subscription_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.triggerSubscription", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "messages_sent": { + "type": "number" + } + }, + "required": ["success", "messages_sent"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/feeds/{feed_id}/subscriptions/{subscription_id}/backfill": { + "post": { + "description": "Start a background job to send historical messages to an agent subscription. Returns immediately with workflow ID. Does not update last_consumed_sequence.", + "summary": "Backfill Subscription", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "subscription_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.backfillSubscription", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "from_sequence": { + "type": "number" + }, + "to_sequence": { + "type": "number" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "workflow_id": { + "type": "string" + } + }, + "required": ["workflow_id"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": ["feedNotFound", "subscriptionNotFound"] + } + }, + "required": ["message", "errorCode"] + } + } + } + } + } + } + }, + "/v1/feeds/{feed_id}/subscriptions/{subscription_id}/history": { + "get": { + "description": "List the run history for a subscription including scheduled runs, manual triggers, and backfills.", + "summary": "List Subscription History", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "subscription_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "page_size", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "next_page_token", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.listSubscriptionHistory", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "runs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "workflow_id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["scheduled", "manual", "backfill"] + }, + "status": { + "type": "string", + "enum": [ + "running", + "completed", + "failed", + "canceled", + "timed_out" + ] + }, + "started_at": { + "type": "string" + }, + "completed_at": { + "type": "string", + "nullable": true + } + }, + "required": [ + "workflow_id", + "type", + "status", + "started_at", + "completed_at" + ] + } + }, + "next_page_token": { + "type": "string", + "nullable": true + } + }, + "required": ["runs", "next_page_token"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": ["feedNotFound", "subscriptionNotFound"] + } + }, + "required": ["message", "errorCode"] + } + } + } + } + } + } + }, + "/v1/feeds/{feed_id}/subscriptions/cron": { + "patch": { + "description": "Update the cron schedule for all subscriptions of a feed", + "summary": "Update All Subscriptions Cron Schedule", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.updateAllSubscriptionsCron", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cron_schedule": { + "type": "string" + } + }, + "required": ["cron_schedule"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "updated_count": { + "type": "number" + } + }, + "required": ["updated_count"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/feeds/{feed_id}/subscriptions": { + "get": { + "description": "List all agent subscriptions for a feed", + "summary": "List Feed Subscriptions", + "tags": ["feeds"], + "parameters": [ + { + "name": "feed_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + { + "name": "agent_id", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "operationId": "feeds.listSubscriptions", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "subscriptions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "feed_id": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "agent_name": { + "type": "string", + "nullable": true + }, + "cron_schedule": { + "type": "string" + }, + "merge_strategy": { + "type": "string", + "enum": [ + "unique-messages", + "combine-into-single-message" + ] + }, + "prompt_template": { + "type": "string", + "nullable": true + }, + "next_scheduled_at": { + "type": "string" + }, + "last_consumed_sequence": { + "type": "number" + }, + "last_consumed_at": { + "type": "string", + "nullable": true + }, + "disabled_at": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "feed_id", + "agent_id", + "agent_name", + "cron_schedule", + "merge_strategy", + "prompt_template", + "next_scheduled_at", + "last_consumed_sequence", + "last_consumed_at", + "disabled_at", + "created_at", + "updated_at" + ] + } + }, + "has_next_page": { + "type": "boolean" + } + }, + "required": ["subscriptions", "has_next_page"] + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/memory-files/directory": { + "get": { + "description": "List immediate children of a directory in the agent memory repo (single level).", + "summary": "List Directory", + "tags": ["memoryFiles"], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "path", + "in": "query", + "description": "Directory path to list. Empty for root.", + "schema": { + "type": "string" + } + }, + { + "name": "depth", + "in": "query", + "description": "Depth of directory listing (default: 1).", + "schema": { + "type": "number", + "nullable": true + } + }, + { + "name": "ref", + "in": "query", + "description": "Git ref (default: HEAD).", + "schema": { + "type": "string" + } + } + ], + "operationId": "memoryFiles.listDirectory", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "entries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["file", "directory"] + } + }, + "required": ["name", "type"] + } + }, + "depth": { + "type": "number" + } + }, + "required": ["path", "entries", "depth"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "501": { + "description": "501", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/memory-files/history": { + "get": { + "description": "Get commit history for a specific file in the agent memory repo.", + "summary": "Get File History", + "tags": ["memoryFiles"], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "file_path", + "in": "query", + "description": "Path to the file (e.g. \"blocks/persona.md\").", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Max commits to return (default: 50).", + "schema": { + "type": "number", + "nullable": true + } + } + ], + "operationId": "memoryFiles.getFileHistory", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "commits": { + "type": "array", + "items": { + "type": "object", + "properties": { + "sha": { + "type": "string" + }, + "message": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "author_name": { + "type": "string", + "nullable": true + } + }, + "required": [ + "sha", + "message", + "timestamp", + "author_name" + ] + } + } + }, + "required": ["path", "commits"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "501": { + "description": "501", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/agents/{agent_id}/memory-files/content": { + "get": { + "description": "Read a single file content at a specific git ref from the agent memory repo.", + "summary": "Read File Content", + "tags": ["memoryFiles"], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "file_path", + "in": "query", + "description": "Path to the file (e.g. \"blocks/persona.md\").", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "ref", + "in": "query", + "description": "Git ref (default: HEAD).", + "schema": { + "type": "string" + } + } + ], + "operationId": "memoryFiles.readFileContent", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "content": { + "type": "string" + }, + "ref": { + "type": "string" + } + }, + "required": ["path", "content", "ref"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + }, + "501": { + "description": "501", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/pipelines": { + "post": { + "description": "Create a new pipeline (producer + feed + optionally subscribers)", + "summary": "Create Pipeline", + "tags": ["pipelines"], + "parameters": [], + "operationId": "pipelines.createPipeline", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "project_id": { + "type": "string" + }, + "integration_type": { + "type": "string", + "enum": [ + "slack", + "discord", + "microsoftTeams", + "custom_webhook" + ] + }, + "producer_config": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["slack_channel_reader"] + }, + "data": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "channel_name": { + "type": "string" + }, + "last_message_ts": { + "type": "string" + } + }, + "required": ["channel_id"] + }, + "minItems": 1, + "maxItems": 100 + }, + "max_messages_per_poll": { + "type": "number" + } + }, + "required": ["channels"] + } + }, + "required": ["type", "data"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["custom_webhook"] + }, + "data": { + "type": "object", + "properties": {} + } + }, + "required": ["type", "data"] + } + ] + }, + "subscriber_agent_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "subscriber_cron_schedule": { + "type": "string" + }, + "prompt_template": { + "type": "string" + } + }, + "required": [ + "name", + "project_id", + "integration_type", + "producer_config" + ] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pipeline": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string" + }, + "project_id": { + "type": "string" + }, + "integration_id": { + "type": "string", + "nullable": true + }, + "integration_type": { + "type": "string", + "enum": [ + "slack", + "discord", + "microsoftTeams", + "custom_webhook" + ] + }, + "feed_id": { + "type": "string" + }, + "config": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["slack_channel_reader"] + }, + "data": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "channel_name": { + "type": "string" + }, + "last_message_ts": { + "type": "string" + } + }, + "required": ["channel_id"] + }, + "minItems": 1, + "maxItems": 100 + }, + "max_messages_per_poll": { + "type": "number" + } + }, + "required": ["channels"] + } + }, + "required": ["type", "data"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["custom_webhook"] + }, + "data": { + "type": "object", + "properties": {} + } + }, + "required": ["type", "data"] + } + ] + }, + "next_scheduled_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_run_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "disabled_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "integration_display_name": { + "type": "string", + "nullable": true + }, + "feed_name": { + "type": "string" + }, + "subscriber_count": { + "type": "number" + }, + "error_count": { + "type": "number" + }, + "project_name": { + "type": "string" + }, + "project_slug": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "organization_id", + "project_id", + "integration_id", + "integration_type", + "feed_id", + "config", + "next_scheduled_at", + "last_run_at", + "disabled_at", + "created_at", + "updated_at" + ] + } + }, + "required": ["pipeline"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": [ + "integrationNotFound", + "invalidProducerConfig", + "agentNotFound" + ] + } + }, + "required": ["message", "errorCode"] + } + } + } + } + } + }, + "get": { + "description": "List all pipelines for the organization with optional filtering", + "summary": "List Pipelines", + "tags": ["pipelines"], + "parameters": [ + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "integration_type", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "integration_id", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "operationId": "pipelines.listPipelines", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pipelines": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string" + }, + "project_id": { + "type": "string" + }, + "integration_id": { + "type": "string", + "nullable": true + }, + "integration_type": { + "type": "string", + "enum": [ + "slack", + "discord", + "microsoftTeams", + "custom_webhook" + ] + }, + "feed_id": { + "type": "string" + }, + "config": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["slack_channel_reader"] + }, + "data": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "channel_name": { + "type": "string" + }, + "last_message_ts": { + "type": "string" + } + }, + "required": ["channel_id"] + }, + "minItems": 1, + "maxItems": 100 + }, + "max_messages_per_poll": { + "type": "number" + } + }, + "required": ["channels"] + } + }, + "required": ["type", "data"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["custom_webhook"] + }, + "data": { + "type": "object", + "properties": {} + } + }, + "required": ["type", "data"] + } + ] + }, + "next_scheduled_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_run_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "disabled_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "integration_display_name": { + "type": "string", + "nullable": true + }, + "feed_name": { + "type": "string" + }, + "subscriber_count": { + "type": "number" + }, + "error_count": { + "type": "number" + }, + "project_name": { + "type": "string" + }, + "project_slug": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "organization_id", + "project_id", + "integration_id", + "integration_type", + "feed_id", + "config", + "next_scheduled_at", + "last_run_at", + "disabled_at", + "created_at", + "updated_at" + ] + } + }, + "hasNextPage": { + "type": "boolean" + } + }, + "required": ["pipelines", "hasNextPage"] + } + } + } + } + } + } + }, + "/v1/pipelines/count": { + "get": { + "description": "Get the total count of pipelines, optionally filtered by project and search", + "summary": "Count Pipelines", + "tags": ["pipelines"], + "parameters": [ + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "integration_type", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "integration_id", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "operationId": "pipelines.countPipelines", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "number" + } + }, + "required": ["count"] + } + } + } + } + } + } + }, + "/v1/pipelines/{pipeline_id}": { + "get": { + "description": "Get a single pipeline with details", + "summary": "Get Pipeline", + "tags": ["pipelines"], + "parameters": [ + { + "name": "pipeline_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "pipelines.getPipeline", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pipeline": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string" + }, + "project_id": { + "type": "string" + }, + "integration_id": { + "type": "string", + "nullable": true + }, + "integration_type": { + "type": "string", + "enum": [ + "slack", + "discord", + "microsoftTeams", + "custom_webhook" + ] + }, + "feed_id": { + "type": "string" + }, + "config": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["slack_channel_reader"] + }, + "data": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "channel_name": { + "type": "string" + }, + "last_message_ts": { + "type": "string" + } + }, + "required": ["channel_id"] + }, + "minItems": 1, + "maxItems": 100 + }, + "max_messages_per_poll": { + "type": "number" + } + }, + "required": ["channels"] + } + }, + "required": ["type", "data"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["custom_webhook"] + }, + "data": { + "type": "object", + "properties": {} + } + }, + "required": ["type", "data"] + } + ] + }, + "next_scheduled_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_run_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "disabled_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "integration_display_name": { + "type": "string", + "nullable": true + }, + "feed_name": { + "type": "string" + }, + "subscriber_count": { + "type": "number" + }, + "error_count": { + "type": "number" + }, + "project_name": { + "type": "string" + }, + "project_slug": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "organization_id", + "project_id", + "integration_id", + "integration_type", + "feed_id", + "config", + "next_scheduled_at", + "last_run_at", + "disabled_at", + "created_at", + "updated_at" + ] + } + }, + "required": ["pipeline"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": ["pipelineNotFound"] + } + }, + "required": ["message", "errorCode"] + } + } + } + } + } + }, + "patch": { + "description": "Update pipeline name or disable/enable it", + "summary": "Update Pipeline", + "tags": ["pipelines"], + "parameters": [ + { + "name": "pipeline_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "pipelines.updatePipeline", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "disabled": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pipeline": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string" + }, + "project_id": { + "type": "string" + }, + "integration_id": { + "type": "string", + "nullable": true + }, + "integration_type": { + "type": "string", + "enum": [ + "slack", + "discord", + "microsoftTeams", + "custom_webhook" + ] + }, + "feed_id": { + "type": "string" + }, + "config": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["slack_channel_reader"] + }, + "data": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "channel_name": { + "type": "string" + }, + "last_message_ts": { + "type": "string" + } + }, + "required": ["channel_id"] + }, + "minItems": 1, + "maxItems": 100 + }, + "max_messages_per_poll": { + "type": "number" + } + }, + "required": ["channels"] + } + }, + "required": ["type", "data"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["custom_webhook"] + }, + "data": { + "type": "object", + "properties": {} + } + }, + "required": ["type", "data"] + } + ] + }, + "next_scheduled_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_run_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "disabled_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "integration_display_name": { + "type": "string", + "nullable": true + }, + "feed_name": { + "type": "string" + }, + "subscriber_count": { + "type": "number" + }, + "error_count": { + "type": "number" + }, + "project_name": { + "type": "string" + }, + "project_slug": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "organization_id", + "project_id", + "integration_id", + "integration_type", + "feed_id", + "config", + "next_scheduled_at", + "last_run_at", + "disabled_at", + "created_at", + "updated_at" + ] + } + }, + "required": ["pipeline"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": ["pipelineNotFound"] + } + }, + "required": ["message"] + } + } + } + } + } + }, + "delete": { + "description": "Soft delete a pipeline and cascade to feed + subscriptions", + "summary": "Delete Pipeline", + "tags": ["pipelines"], + "parameters": [ + { + "name": "pipeline_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "pipelines.deletePipeline", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": ["pipelineNotFound"] + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/pipelines/{pipeline_id}/config": { + "patch": { + "description": "Update the producer configuration for a pipeline (e.g., Slack channels)", + "summary": "Update Pipeline Producer Config", + "tags": ["pipelines"], + "parameters": [ + { + "name": "pipeline_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "pipelines.updatePipelineProducerConfig", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "producer_config": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["slack_channel_reader"] + }, + "data": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "channel_name": { + "type": "string" + }, + "last_message_ts": { + "type": "string" + } + }, + "required": ["channel_id"] + }, + "minItems": 1, + "maxItems": 100 + }, + "max_messages_per_poll": { + "type": "number" + } + }, + "required": ["channels"] + } + }, + "required": ["type", "data"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["custom_webhook"] + }, + "data": { + "type": "object", + "properties": {} + } + }, + "required": ["type", "data"] + } + ] + } + }, + "required": ["producer_config"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pipeline": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organization_id": { + "type": "string" + }, + "project_id": { + "type": "string" + }, + "integration_id": { + "type": "string", + "nullable": true + }, + "integration_type": { + "type": "string", + "enum": [ + "slack", + "discord", + "microsoftTeams", + "custom_webhook" + ] + }, + "feed_id": { + "type": "string" + }, + "config": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["slack_channel_reader"] + }, + "data": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "channel_name": { + "type": "string" + }, + "last_message_ts": { + "type": "string" + } + }, + "required": ["channel_id"] + }, + "minItems": 1, + "maxItems": 100 + }, + "max_messages_per_poll": { + "type": "number" + } + }, + "required": ["channels"] + } + }, + "required": ["type", "data"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["custom_webhook"] + }, + "data": { + "type": "object", + "properties": {} + } + }, + "required": ["type", "data"] + } + ] + }, + "next_scheduled_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_run_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "disabled_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "integration_display_name": { + "type": "string", + "nullable": true + }, + "feed_name": { + "type": "string" + }, + "subscriber_count": { + "type": "number" + }, + "error_count": { + "type": "number" + }, + "project_name": { + "type": "string" + }, + "project_slug": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "organization_id", + "project_id", + "integration_id", + "integration_type", + "feed_id", + "config", + "next_scheduled_at", + "last_run_at", + "disabled_at", + "created_at", + "updated_at" + ] + } + }, + "required": ["pipeline"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": ["invalidProducerConfig", "configTypeMismatch"] + } + }, + "required": ["message", "errorCode"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": ["pipelineNotFound"] + } + }, + "required": ["message", "errorCode"] + } + } + } + } + } + } + }, + "/v1/pipelines/preview": { + "post": { + "description": "Fetch sample messages from integration to preview what agents will receive", + "summary": "Preview Pipeline", + "tags": ["pipelines"], + "parameters": [], + "operationId": "pipelines.previewPipeline", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "integration_type": { + "type": "string", + "enum": [ + "slack", + "discord", + "microsoftTeams", + "custom_webhook" + ] + }, + "integration_id": { + "type": "string" + }, + "producer_config": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["slack_channel_reader"] + }, + "data": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": { + "type": "string" + }, + "channel_name": { + "type": "string" + }, + "last_message_ts": { + "type": "string" + } + }, + "required": ["channel_id"] + }, + "minItems": 1, + "maxItems": 100 + }, + "max_messages_per_poll": { + "type": "number" + } + }, + "required": ["channels"] + } + }, + "required": ["type", "data"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["custom_webhook"] + }, + "data": { + "type": "object", + "properties": {} + } + }, + "required": ["type", "data"] + } + ] + } + }, + "required": [ + "integration_type", + "integration_id", + "producer_config" + ] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sampleMessages": { + "type": "array", + "items": { + "type": "string" + } + }, + "messageCount": { + "type": "number" + } + }, + "required": ["sampleMessages", "messageCount"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": [ + "integrationNotFound", + "invalidProducerConfig", + "tokenExpired" + ] + } + }, + "required": ["message"] + } + } + } + } + } + } + }, + "/v1/pipelines/{pipeline_id}/sync": { + "post": { + "description": "Manually trigger a pipeline sync to fetch new messages immediately", + "summary": "Sync Pipeline", + "tags": ["pipelines"], + "parameters": [ + { + "name": "pipeline_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "pipelines.syncPipeline", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "messages_ingested": { + "type": "number" + }, + "workflow_id": { + "type": "string" + } + }, + "required": ["success", "messages_ingested", "workflow_id"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": [ + "pipelineDisabled", + "pipelineNotSyncable", + "syncFailed" + ] + } + }, + "required": ["message", "errorCode"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": ["pipelineNotFound"] + } + }, + "required": ["message", "errorCode"] + } + } + } + } + } + } + }, + "/v1/pipelines/{pipeline_id}/sync/history": { + "get": { + "description": "List the sync run history for a pipeline from Temporal with error details", + "summary": "List Pipeline Sync History", + "tags": ["pipelines"], + "parameters": [ + { + "name": "pipeline_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "pipelines.listPipelineSyncHistory", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "runs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "workflow_id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "running", + "completed", + "failed", + "canceled", + "timed_out" + ] + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "error": { + "type": "object", + "properties": { + "error_type": { + "type": "string" + }, + "error_message": { + "type": "string" + } + }, + "required": ["error_type", "error_message"], + "nullable": true + } + }, + "required": [ + "workflow_id", + "status", + "started_at", + "completed_at", + "error" + ] + } + }, + "next_page_token": { + "type": "string", + "nullable": true + } + }, + "required": ["runs", "next_page_token"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "errorCode": { + "type": "string", + "enum": ["pipelineNotFound"] + } + }, + "required": ["message", "errorCode"] + } + } + } + } + } + } + }, + "/v1/environments/register": { + "post": { + "description": "Register a new listener connection and get connectionId for WebSocket", + "summary": "Register Environment", + "tags": ["environments"], + "parameters": [], + "operationId": "environments.register", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deviceId": { + "type": "string" + }, + "connectionName": { + "type": "string" + }, + "metadata": { + "type": "object", + "properties": { + "os": { + "type": "string" + }, + "lettaCodeVersion": { + "type": "string" + }, + "nodeVersion": { + "type": "string" + }, + "workingDirectory": { + "type": "string" + }, + "gitBranch": { + "type": "string" + } + }, + "additionalProperties": true + } + }, + "required": ["deviceId", "connectionName"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connectionId": { + "type": "string" + }, + "wsUrl": { + "type": "string" + } + }, + "required": ["connectionId", "wsUrl"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + } + } + } + }, + "/v1/environments/{deviceId}": { + "get": { + "description": "Get a specific environment connection by deviceId", + "summary": "Get Environment Connection", + "tags": ["environments"], + "parameters": [ + { + "name": "deviceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "environments.getConnection", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "connectionId": { + "type": "string", + "nullable": true + }, + "deviceId": { + "type": "string" + }, + "connectionName": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "apiKeyOwner": { + "type": "string" + }, + "podId": { + "type": "string", + "nullable": true + }, + "connectedAt": { + "type": "number", + "nullable": true + }, + "lastHeartbeat": { + "type": "number", + "nullable": true + }, + "lastSeenAt": { + "type": "number" + }, + "firstSeenAt": { + "type": "number" + }, + "currentMode": { + "type": "string", + "enum": [ + "default", + "acceptEdits", + "plan", + "bypassPermissions" + ] + }, + "metadata": { + "type": "object", + "properties": { + "os": { + "type": "string" + }, + "lettaCodeVersion": { + "type": "string" + }, + "nodeVersion": { + "type": "string" + }, + "workingDirectory": { + "type": "string" + }, + "gitBranch": { + "type": "string" + } + }, + "additionalProperties": true + } + }, + "required": [ + "id", + "connectionId", + "deviceId", + "connectionName", + "organizationId", + "podId", + "connectedAt", + "lastHeartbeat", + "lastSeenAt", + "firstSeenAt" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + } + } + } + }, + "/v1/environments": { + "get": { + "description": "List all active environment connections for the organization", + "summary": "List Environment Connections", + "tags": ["environments"], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "after", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "onlineOnly", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "operationId": "environments.listConnections", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "connectionId": { + "type": "string", + "nullable": true + }, + "deviceId": { + "type": "string" + }, + "connectionName": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "apiKeyOwner": { + "type": "string" + }, + "podId": { + "type": "string", + "nullable": true + }, + "connectedAt": { + "type": "number", + "nullable": true + }, + "lastHeartbeat": { + "type": "number", + "nullable": true + }, + "lastSeenAt": { + "type": "number" + }, + "firstSeenAt": { + "type": "number" + }, + "currentMode": { + "type": "string", + "enum": [ + "default", + "acceptEdits", + "plan", + "bypassPermissions" + ] + }, + "metadata": { + "type": "object", + "properties": { + "os": { + "type": "string" + }, + "lettaCodeVersion": { + "type": "string" + }, + "nodeVersion": { + "type": "string" + }, + "workingDirectory": { + "type": "string" + }, + "gitBranch": { + "type": "string" + } + }, + "additionalProperties": true + } + }, + "required": [ + "id", + "connectionId", + "deviceId", + "connectionName", + "organizationId", + "podId", + "connectedAt", + "lastHeartbeat", + "lastSeenAt", + "firstSeenAt" + ] + } + }, + "hasNextPage": { + "type": "boolean" + } + }, + "required": ["connections", "hasNextPage"] + } + } + } + } + } + } + }, + "/v1/environments/{connectionId}/messages": { + "post": { + "description": "Send a message to a specific environment connection", + "summary": "Send Message to Environment", + "tags": ["environments"], + "parameters": [ + { + "name": "connectionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "environments.sendMessage", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["user"] + }, + "content": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["text"] + }, + "text": { + "type": "string" + } + }, + "required": ["type", "text"] + } + } + ] + }, + "client_message_id": { + "type": "string" + }, + "otid": { + "type": "string" + } + }, + "required": ["role", "content", "client_message_id"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["approval"] + }, + "approvals": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["tool"] + }, + "tool_call_id": { + "type": "string" + }, + "tool_return": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["text"] + }, + "text": { + "type": "string" + } + }, + "required": ["type", "text"] + } + } + ] + }, + "status": { + "type": "string", + "enum": ["success", "error"] + }, + "stdout": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "stderr": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "required": [ + "tool_call_id", + "tool_return", + "status" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["approval"] + }, + "approve": { + "type": "boolean" + }, + "tool_call_id": { + "type": "string" + }, + "reason": { + "type": "string", + "nullable": true + }, + "updated_input": { + "type": "object", + "additionalProperties": {}, + "nullable": true + } + }, + "required": ["approve", "tool_call_id"] + } + ] + } + } + }, + "required": ["type", "approvals"] + } + ] + } + }, + "agentId": { + "type": "string" + }, + "conversationId": { + "type": "string", + "nullable": true + } + }, + "required": ["messages"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success", "message"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + }, + "503": { + "description": "503", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + } + } + } + }, + "/v1/environments/{id}": { + "delete": { + "description": "Removes environment from list of environments", + "summary": "Delete Environment", + "tags": ["environments"], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "environments.deleteEnvironment", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {}, + "nullable": true + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success", "message"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + } + } + } + }, + "/v1/sandboxes": { + "post": { + "description": "Create a new Modal Sandbox that runs letta remote automatically", + "summary": "Create Sandbox", + "tags": ["sandboxes"], + "parameters": [], + "operationId": "sandboxes.createSandbox", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agentId": { + "type": "string" + }, + "connectionName": { + "type": "string" + } + }, + "required": ["agentId"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sandboxId": { + "type": "string" + }, + "deviceId": { + "type": "string" + }, + "connectionName": { + "type": "string" + } + }, + "required": ["sandboxId", "deviceId", "connectionName"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + } + } + }, + "get": { + "description": "List all sandboxes for the organization", + "summary": "List Sandboxes", + "tags": ["sandboxes"], + "parameters": [ + { + "name": "agentId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "operationId": "sandboxes.listSandboxes", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sandboxes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sandboxId": { + "type": "string" + }, + "agentId": { + "type": "string" + }, + "connectionName": { + "type": "string" + }, + "deviceId": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "connectionId": { + "type": "string", + "nullable": true + }, + "podId": { + "type": "string", + "nullable": true + }, + "connectedAt": { + "type": "number", + "nullable": true + }, + "lastHeartbeat": { + "type": "number", + "nullable": true + }, + "lastSeenAt": { + "type": "number" + }, + "firstSeenAt": { + "type": "number" + }, + "currentMode": { + "type": "string", + "enum": [ + "default", + "acceptEdits", + "plan", + "bypassPermissions" + ] + }, + "metadata": { + "type": "object", + "properties": { + "os": { + "type": "string" + }, + "lettaCodeVersion": { + "type": "string" + }, + "nodeVersion": { + "type": "string" + }, + "workingDirectory": { + "type": "string" + }, + "gitBranch": { + "type": "string" + } + }, + "additionalProperties": true + } + }, + "required": [ + "id", + "sandboxId", + "agentId", + "connectionName", + "deviceId", + "organizationId", + "connectionId", + "podId", + "connectedAt", + "lastHeartbeat", + "lastSeenAt", + "firstSeenAt" + ] + } + } + }, + "required": ["sandboxes"] + } + } + } + } + } + } + }, + "/v1/sandboxes/{sandboxId}/terminate": { + "post": { + "description": "Terminate a Modal Sandbox", + "summary": "Terminate Sandbox", + "tags": ["sandboxes"], + "parameters": [ + { + "name": "sandboxId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "sandboxes.terminateSandbox", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {}, + "nullable": true + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success", "message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + } + } + } + }, + "/v1/device-storage-key": { + "get": { + "description": "Returns an HMAC-derived AES-256-GCM key scoped to the authenticated user and device. Used to encrypt/decrypt local IndexedDB caches on the client.", + "summary": "Get Device Storage Key", + "tags": ["deviceStorage"], + "parameters": [ + { + "name": "deviceId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "deviceStorage.getDeviceStorageKey", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string" + } + }, + "required": ["key"] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AgentEnvironmentVariable": { + "properties": { + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this object." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this object." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "The timestamp when the object was created." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the object was last updated." + }, + "id": { + "type": "string", + "pattern": "^agent-env-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Agent-env", + "examples": ["agent-env-123e4567-e89b-12d3-a456-426614174000"] + }, + "key": { + "type": "string", + "title": "Key", + "description": "The name of the environment variable." + }, + "value": { + "type": "string", + "title": "Value", + "description": "The value of the environment variable." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "An optional description of the environment variable." + }, + "value_enc": { + "anyOf": [ + { + "type": "string", + "description": "Encrypted secret value (stored as encrypted string)", + "nullable": true + }, + { + "type": "null" + } + ], + "title": "Value Enc", + "description": "Encrypted value as Secret object" + }, + "agent_id": { + "type": "string", + "title": "Agent Id", + "description": "The ID of the agent this environment variable belongs to." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["key", "value", "agent_id"], + "title": "AgentEnvironmentVariable" + }, + "AgentFileAttachment": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Unique identifier of the file-agent relationship" + }, + "file_id": { + "type": "string", + "title": "File Id", + "description": "Unique identifier of the file" + }, + "file_name": { + "type": "string", + "title": "File Name", + "description": "Name of the file" + }, + "folder_id": { + "type": "string", + "title": "Folder Id", + "description": "Unique identifier of the folder/source" + }, + "folder_name": { + "type": "string", + "title": "Folder Name", + "description": "Name of the folder/source" + }, + "is_open": { + "type": "boolean", + "title": "Is Open", + "description": "Whether the file is currently open in the agent's context" + }, + "last_accessed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Accessed At", + "description": "Timestamp of last access by the agent" + }, + "visible_content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Visible Content", + "description": "Portion of the file visible to the agent if open" + }, + "start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Start Line", + "description": "Starting line number if file was opened with line range" + }, + "end_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "End Line", + "description": "Ending line number if file was opened with line range" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "file_id", + "file_name", + "folder_id", + "folder_name", + "is_open" + ], + "title": "AgentFileAttachment", + "description": "Response model for agent file attachments showing file status in agent context" + }, + "AgentFileSchema": { + "properties": { + "agents": { + "items": { + "$ref": "#/components/schemas/letta__schemas__agent_file__AgentSchema" + }, + "type": "array", + "title": "Agents", + "description": "List of agents in this agent file" + }, + "groups": { + "items": { + "$ref": "#/components/schemas/GroupSchema" + }, + "type": "array", + "title": "Groups", + "description": "List of groups in this agent file" + }, + "blocks": { + "items": { + "$ref": "#/components/schemas/BlockSchema" + }, + "type": "array", + "title": "Blocks", + "description": "List of memory blocks in this agent file" + }, + "files": { + "items": { + "$ref": "#/components/schemas/FileSchema" + }, + "type": "array", + "title": "Files", + "description": "List of files in this agent file" + }, + "sources": { + "items": { + "$ref": "#/components/schemas/SourceSchema" + }, + "type": "array", + "title": "Sources", + "description": "List of sources in this agent file" + }, + "tools": { + "items": { + "$ref": "#/components/schemas/letta__schemas__agent_file__ToolSchema" + }, + "type": "array", + "title": "Tools", + "description": "List of tools in this agent file" + }, + "mcp_servers": { + "items": { + "$ref": "#/components/schemas/MCPServerSchema" + }, + "type": "array", + "title": "Mcp Servers", + "description": "List of MCP servers in this agent file" + }, + "skills": { + "items": { + "$ref": "#/components/schemas/SkillSchema" + }, + "type": "array", + "title": "Skills", + "description": "List of skills in this agent file" + }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Metadata", + "description": "Metadata for this agent file, including revision_id and other export information." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "The timestamp when the object was created." + } + }, + "type": "object", + "required": [ + "agents", + "groups", + "blocks", + "files", + "sources", + "tools", + "mcp_servers" + ], + "title": "AgentFileSchema", + "description": "Schema for serialized agent file that can be exported to JSON and imported into agent server." + }, + "AgentState": { + "properties": { + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this object." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this object." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "The timestamp when the object was created." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the object was last updated." + }, + "id": { + "type": "string", + "title": "Id", + "description": "The id of the agent. Assigned by the database." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the agent." + }, + "tool_rules": { + "anyOf": [ + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ChildToolRule" + }, + { + "$ref": "#/components/schemas/InitToolRule" + }, + { + "$ref": "#/components/schemas/TerminalToolRule" + }, + { + "$ref": "#/components/schemas/ConditionalToolRule" + }, + { + "$ref": "#/components/schemas/ContinueToolRule" + }, + { + "$ref": "#/components/schemas/RequiredBeforeExitToolRule" + }, + { + "$ref": "#/components/schemas/MaxCountPerStepToolRule" + }, + { + "$ref": "#/components/schemas/ParentToolRule" + }, + { + "$ref": "#/components/schemas/RequiresApprovalToolRule" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "conditional": "#/components/schemas/ConditionalToolRule", + "constrain_child_tools": "#/components/schemas/ChildToolRule", + "continue_loop": "#/components/schemas/ContinueToolRule", + "exit_loop": "#/components/schemas/TerminalToolRule", + "max_count_per_step": "#/components/schemas/MaxCountPerStepToolRule", + "parent_last_tool": "#/components/schemas/ParentToolRule", + "required_before_exit": "#/components/schemas/RequiredBeforeExitToolRule", + "requires_approval": "#/components/schemas/RequiresApprovalToolRule", + "run_first": "#/components/schemas/InitToolRule" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Rules", + "description": "The list of tool rules." + }, + "message_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Message Ids", + "description": "The ids of the messages in the agent's in-context memory." + }, + "system": { + "type": "string", + "title": "System", + "description": "The system prompt used by the agent." + }, + "agent_type": { + "$ref": "#/components/schemas/AgentType", + "description": "The type of agent." + }, + "llm_config": { + "$ref": "#/components/schemas/LLMConfig", + "description": "Deprecated: Use `model` field instead. The LLM configuration used by the agent.", + "deprecated": true + }, + "embedding_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use `embedding` field instead. The embedding configuration used by the agent.", + "deprecated": true + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "The model handle used by the agent (format: provider/model-name)." + }, + "embedding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Embedding", + "description": "The embedding model handle used by the agent (format: provider/model-name)." + }, + "model_settings": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/OpenAIModelSettings" + }, + { + "$ref": "#/components/schemas/SGLangModelSettings" + }, + { + "$ref": "#/components/schemas/AnthropicModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleAIModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleVertexModelSettings" + }, + { + "$ref": "#/components/schemas/AzureModelSettings" + }, + { + "$ref": "#/components/schemas/XAIModelSettings" + }, + { + "$ref": "#/components/schemas/ZAIModelSettings" + }, + { + "$ref": "#/components/schemas/GroqModelSettings" + }, + { + "$ref": "#/components/schemas/DeepseekModelSettings" + }, + { + "$ref": "#/components/schemas/TogetherModelSettings" + }, + { + "$ref": "#/components/schemas/BedrockModelSettings" + }, + { + "$ref": "#/components/schemas/BasetenModelSettings" + }, + { + "$ref": "#/components/schemas/OpenRouterModelSettings" + }, + { + "$ref": "#/components/schemas/ChatGPTOAuthModelSettings" + } + ], + "discriminator": { + "propertyName": "provider_type", + "mapping": { + "anthropic": "#/components/schemas/AnthropicModelSettings", + "azure": "#/components/schemas/AzureModelSettings", + "baseten": "#/components/schemas/BasetenModelSettings", + "bedrock": "#/components/schemas/BedrockModelSettings", + "chatgpt_oauth": "#/components/schemas/ChatGPTOAuthModelSettings", + "deepseek": "#/components/schemas/DeepseekModelSettings", + "google_ai": "#/components/schemas/GoogleAIModelSettings", + "google_vertex": "#/components/schemas/GoogleVertexModelSettings", + "groq": "#/components/schemas/GroqModelSettings", + "openai": "#/components/schemas/OpenAIModelSettings", + "openrouter": "#/components/schemas/OpenRouterModelSettings", + "sglang": "#/components/schemas/SGLangModelSettings", + "together": "#/components/schemas/TogetherModelSettings", + "xai": "#/components/schemas/XAIModelSettings", + "zai": "#/components/schemas/ZAIModelSettings" + } + } + }, + { + "type": "null" + } + ], + "title": "Model Settings", + "description": "The model settings used by the agent." + }, + "compaction_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/CompactionSettings-Output" + }, + { + "type": "null" + } + ], + "description": "The compaction settings configuration used for compaction." + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format used by the agent" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the agent." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "The metadata of the agent." + }, + "memory": { + "$ref": "#/components/schemas/Memory", + "description": "Deprecated: Use `blocks` field instead. The in-context memory of the agent.", + "deprecated": true + }, + "blocks": { + "items": { + "$ref": "#/components/schemas/Block" + }, + "type": "array", + "title": "Blocks", + "description": "The memory blocks used by the agent." + }, + "tools": { + "items": { + "$ref": "#/components/schemas/Tool" + }, + "type": "array", + "title": "Tools", + "description": "The tools used by the agent." + }, + "sources": { + "items": { + "$ref": "#/components/schemas/Source" + }, + "type": "array", + "title": "Sources", + "description": "Deprecated: Use `folders` field instead. The sources used by the agent.", + "deprecated": true + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags", + "description": "The tags associated with the agent." + }, + "tool_exec_environment_variables": { + "items": { + "$ref": "#/components/schemas/AgentEnvironmentVariable" + }, + "type": "array", + "title": "Tool Exec Environment Variables", + "description": "Deprecated: use `secrets` field instead.", + "deprecated": true + }, + "secrets": { + "items": { + "$ref": "#/components/schemas/AgentEnvironmentVariable" + }, + "type": "array", + "title": "Secrets", + "description": "The environment variables for tool execution specific to this agent." + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The id of the project the agent belongs to." + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template the agent belongs to." + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "The base template id of the agent." + }, + "deployment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deployment Id", + "description": "The id of the deployment." + }, + "entity_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Entity Id", + "description": "The id of the entity within the template." + }, + "identity_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Identity Ids", + "description": "Deprecated: Use `identities` field instead. The ids of the identities associated with this agent.", + "default": [], + "deprecated": true + }, + "identities": { + "items": { + "$ref": "#/components/schemas/Identity" + }, + "type": "array", + "title": "Identities", + "description": "The identities associated with this agent.", + "default": [] + }, + "pending_approval": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApprovalRequestMessage" + }, + { + "type": "null" + } + ], + "description": "The latest approval request message pending for this agent, if any." + }, + "message_buffer_autoclear": { + "type": "boolean", + "title": "Message Buffer Autoclear", + "description": "If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case.", + "default": false + }, + "enable_sleeptime": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Sleeptime", + "description": "If set to True, memory management will move to a background agent thread." + }, + "multi_agent_group": { + "anyOf": [ + { + "$ref": "#/components/schemas/Group" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use `managed_group` field instead. The multi-agent group that this agent manages.", + "deprecated": true + }, + "managed_group": { + "anyOf": [ + { + "$ref": "#/components/schemas/Group" + }, + { + "type": "null" + } + ], + "description": "The multi-agent group that this agent manages" + }, + "last_run_completion": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Run Completion", + "description": "The timestamp when the agent last completed a run." + }, + "last_run_duration_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Last Run Duration Ms", + "description": "The duration in milliseconds of the agent's last run." + }, + "last_stop_reason": { + "anyOf": [ + { + "$ref": "#/components/schemas/StopReasonType" + }, + { + "type": "null" + } + ], + "description": "The stop reason from the agent's last run." + }, + "timezone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The timezone of the agent (IANA format)." + }, + "max_files_open": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Files Open", + "description": "Maximum number of files that can be open at once for this agent. Setting this too high may exceed the context window, which will break the agent." + }, + "per_file_view_window_char_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Per File View Window Char Limit", + "description": "The per-file view window character limit for this agent. Setting this too high may exceed the context window, which will break the agent." + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "If set to True, the agent will be hidden." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "name", + "system", + "agent_type", + "llm_config", + "memory", + "blocks", + "tools", + "sources", + "tags" + ], + "title": "AgentState", + "description": "Representation of an agent's state. This is the state of the agent at a given time, and is persisted in the DB backend. The state has all the information needed to recreate a persisted agent." + }, + "AgentType": { + "type": "string", + "enum": [ + "memgpt_agent", + "memgpt_v2_agent", + "letta_v1_agent", + "react_agent", + "workflow_agent", + "split_thread_agent", + "sleeptime_agent", + "voice_convo_agent", + "voice_sleeptime_agent" + ], + "title": "AgentType", + "description": "Enum to represent the type of agent." + }, + "Annotation": { + "properties": { + "type": { + "type": "string", + "const": "url_citation", + "title": "Type" + }, + "url_citation": { + "$ref": "#/components/schemas/AnnotationURLCitation" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["type", "url_citation"], + "title": "Annotation", + "description": "A URL citation when using web search." + }, + "AnnotationURLCitation": { + "properties": { + "end_index": { + "type": "integer", + "title": "End Index" + }, + "start_index": { + "type": "integer", + "title": "Start Index" + }, + "title": { + "type": "string", + "title": "Title" + }, + "url": { + "type": "string", + "title": "Url" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["end_index", "start_index", "title", "url"], + "title": "AnnotationURLCitation", + "description": "A URL citation when using web search." + }, + "AnthropicModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "anthropic", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "anthropic" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 1 + }, + "thinking": { + "$ref": "#/components/schemas/AnthropicThinking", + "description": "The thinking configuration for the model.", + "default": { + "type": "enabled", + "budget_tokens": 1024 + } + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model." + }, + "verbosity": { + "anyOf": [ + { + "type": "string", + "enum": ["low", "medium", "high"] + }, + { + "type": "null" + } + ], + "title": "Verbosity", + "description": "Soft control for how verbose model output should be, used for GPT-5 models." + }, + "effort": { + "anyOf": [ + { + "type": "string", + "enum": ["low", "medium", "high", "max"] + }, + { + "type": "null" + } + ], + "title": "Effort", + "description": "Effort level for supported Anthropic models (controls token spending). 'max' is only available on Opus 4.6. Not setting this gives similar performance to 'high'." + }, + "strict": { + "type": "boolean", + "title": "Strict", + "description": "Enable strict mode for tool calling. When true, tool outputs are guaranteed to match JSON schemas.", + "default": false + } + }, + "type": "object", + "title": "AnthropicModelSettings" + }, + "AnthropicThinking": { + "properties": { + "type": { + "type": "string", + "enum": ["enabled", "disabled"], + "title": "Type", + "description": "The type of thinking to use.", + "default": "enabled" + }, + "budget_tokens": { + "type": "integer", + "title": "Budget Tokens", + "description": "The maximum number of tokens the model can use for extended thinking.", + "default": 1024 + } + }, + "type": "object", + "title": "AnthropicThinking" + }, + "ApprovalCreate": { + "properties": { + "type": { + "type": "string", + "const": "approval", + "title": "Type", + "description": "The message type to be created.", + "default": "approval" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "group_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Group Id", + "description": "The multi-agent group that the message was sent in" + }, + "approvals": { + "anyOf": [ + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApprovalReturn" + }, + { + "$ref": "#/components/schemas/letta__schemas__letta_message__ToolReturn" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "approval": "#/components/schemas/ApprovalReturn", + "tool": "#/components/schemas/letta__schemas__letta_message__ToolReturn" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Approvals", + "description": "The list of approval responses" + }, + "approve": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Approve", + "description": "Whether the tool has been approved", + "deprecated": true + }, + "approval_request_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Approval Request Id", + "description": "The message ID of the approval request", + "deprecated": true + }, + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reason", + "description": "An optional explanation for the provided approval status", + "deprecated": true + } + }, + "type": "object", + "title": "ApprovalCreate", + "description": "Input to approve or deny a tool call request" + }, + "ApprovalRequestMessage": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "message_type": { + "type": "string", + "const": "approval_request_message", + "title": "Message Type", + "description": "The type of the message.", + "default": "approval_request_message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err" + }, + "seq_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seq Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + }, + "tool_call": { + "anyOf": [ + { + "$ref": "#/components/schemas/ToolCall" + }, + { + "$ref": "#/components/schemas/ToolCallDelta" + } + ], + "title": "Tool Call", + "description": "The tool call that has been requested by the llm to run", + "deprecated": true + }, + "tool_calls": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ToolCall" + }, + "type": "array" + }, + { + "$ref": "#/components/schemas/ToolCallDelta" + }, + { + "type": "null" + } + ], + "title": "Tool Calls", + "description": "The tool calls that have been requested by the llm to run, which are pending approval" + } + }, + "type": "object", + "required": ["id", "date", "tool_call"], + "title": "ApprovalRequestMessage", + "description": "A message representing a request for approval to call a tool (generated by the LLM to trigger tool execution).\n\nArgs:\n id (str): The ID of the message\n date (datetime): The date the message was created in ISO format\n name (Optional[str]): The name of the sender of the message\n tool_call (ToolCall): The tool call" + }, + "ApprovalResponseMessage": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "message_type": { + "type": "string", + "const": "approval_response_message", + "title": "Message Type", + "description": "The type of the message.", + "default": "approval_response_message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err" + }, + "seq_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seq Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + }, + "approvals": { + "anyOf": [ + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApprovalReturn" + }, + { + "$ref": "#/components/schemas/letta__schemas__letta_message__ToolReturn" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "approval": "#/components/schemas/ApprovalReturn", + "tool": "#/components/schemas/letta__schemas__letta_message__ToolReturn" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Approvals", + "description": "The list of approval responses" + }, + "approve": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Approve", + "description": "Whether the tool has been approved", + "deprecated": true + }, + "approval_request_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Approval Request Id", + "description": "The message ID of the approval request", + "deprecated": true + }, + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reason", + "description": "An optional explanation for the provided approval status", + "deprecated": true + } + }, + "type": "object", + "required": ["id", "date"], + "title": "ApprovalResponseMessage", + "description": "A message representing a response form the user indicating whether a tool has been approved to run.\n\nArgs:\n id (str): The ID of the message\n date (datetime): The date the message was created in ISO format\n name (Optional[str]): The name of the sender of the message\n approve: (bool) Whether the tool has been approved\n approval_request_id: The ID of the approval request\n reason: (Optional[str]) An optional explanation for the provided approval status" + }, + "ApprovalReturn": { + "properties": { + "type": { + "type": "string", + "const": "approval", + "title": "Type", + "description": "The message type to be created.", + "default": "approval" + }, + "tool_call_id": { + "type": "string", + "title": "Tool Call Id", + "description": "The ID of the tool call that corresponds to this approval" + }, + "approve": { + "type": "boolean", + "title": "Approve", + "description": "Whether the tool has been approved" + }, + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reason", + "description": "An optional explanation for the provided approval status" + } + }, + "type": "object", + "required": ["tool_call_id", "approve"], + "title": "ApprovalReturn" + }, + "ArchivalMemorySearchResponse": { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/ArchivalMemorySearchResult" + }, + "type": "array", + "title": "Results", + "description": "List of search results matching the query" + }, + "count": { + "type": "integer", + "title": "Count", + "description": "Total number of results returned" + } + }, + "type": "object", + "required": ["results", "count"], + "title": "ArchivalMemorySearchResponse" + }, + "ArchivalMemorySearchResult": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Unique identifier of the archival memory passage" + }, + "timestamp": { + "type": "string", + "title": "Timestamp", + "description": "Timestamp of when the memory was created, formatted in agent's timezone" + }, + "content": { + "type": "string", + "title": "Content", + "description": "Text content of the archival memory passage" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags", + "description": "List of tags associated with this memory" + } + }, + "type": "object", + "required": ["id", "timestamp", "content"], + "title": "ArchivalMemorySearchResult" + }, + "Archive": { + "properties": { + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this object." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this object." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The creation date of the archive" + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the object was last updated." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the archive" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "A description of the archive" + }, + "vector_db_provider": { + "$ref": "#/components/schemas/VectorDBProvider", + "description": "The vector database provider used for this archive's passages", + "default": "native" + }, + "embedding_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + { + "type": "null" + } + ], + "description": "Embedding configuration for passages in this archive" + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Additional metadata" + }, + "id": { + "type": "string", + "pattern": "^archive-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Archive", + "examples": ["archive-123e4567-e89b-12d3-a456-426614174000"] + } + }, + "additionalProperties": false, + "type": "object", + "required": ["created_at", "name", "organization_id"], + "title": "Archive", + "description": "Representation of an archive - a collection of archival passages that can be shared between agents." + }, + "ArchiveCreateRequest": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "embedding_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use `embedding` field instead. Embedding configuration for the archive", + "deprecated": true + }, + "embedding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Embedding", + "description": "Embedding model handle for the archive" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "type": "object", + "required": ["name"], + "title": "ArchiveCreateRequest", + "description": "Request model for creating an archive.\n\nIntentionally excludes vector_db_provider. These are derived internally (vector DB provider from env)." + }, + "ArchiveUpdateRequest": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "type": "object", + "title": "ArchiveUpdateRequest", + "description": "Request model for updating an archive (partial).\n\nSupports updating only name and description." + }, + "AssistantMessage": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "message_type": { + "type": "string", + "const": "assistant_message", + "title": "Message Type", + "description": "The type of the message.", + "default": "assistant_message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err" + }, + "seq_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seq Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + }, + "content": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/LettaAssistantMessageContentUnion" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "title": "Content", + "description": "The message content sent by the agent (can be a string or an array of content parts)" + } + }, + "type": "object", + "required": ["id", "date", "content"], + "title": "AssistantMessage", + "description": "A message sent by the LLM in response to user input. Used in the LLM context.\n\nArgs:\n id (str): The ID of the message\n date (datetime): The date the message was created in ISO format\n name (Optional[str]): The name of the sender of the message\n content (Union[str, List[LettaAssistantMessageContentUnion]]): The message content sent by the agent (can be a string or an array of content parts)" + }, + "AssistantMessageListResult": { + "properties": { + "message_type": { + "type": "string", + "const": "assistant_message", + "title": "Message Type", + "default": "assistant_message" + }, + "content": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/LettaAssistantMessageContentUnion" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "title": "Content", + "description": "The message content sent by the assistant (can be a string or an array of content parts)" + }, + "message_id": { + "type": "string", + "title": "Message Id", + "description": "The unique identifier of the message." + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "The unique identifier of the agent that owns the message." + }, + "conversation_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Conversation Id", + "description": "The unique identifier of the conversation that the message belongs to." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The time the message was created in ISO format." + } + }, + "type": "object", + "required": ["content", "message_id", "created_at"], + "title": "AssistantMessageListResult", + "description": "Assistant message list result with agent context.\n\nShape is identical to UpdateAssistantMessage but includes the owning agent_id and message id." + }, + "Audio": { + "properties": { + "id": { + "type": "string", + "title": "Id" + } + }, + "type": "object", + "required": ["id"], + "title": "Audio", + "description": "Data about a previous audio response from the model.\n[Learn more](https://platform.openai.com/docs/guides/audio)." + }, + "AuthRequest": { + "properties": { + "password": { + "type": "string", + "title": "Password", + "description": "Admin password provided when starting the Letta server" + } + }, + "type": "object", + "title": "AuthRequest" + }, + "AuthResponse": { + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "title": "Uuid", + "description": "UUID of the user" + }, + "is_admin": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Admin", + "description": "Whether the user is an admin" + } + }, + "type": "object", + "required": ["uuid"], + "title": "AuthResponse" + }, + "AzureModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "azure", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "azure" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model." + } + }, + "type": "object", + "title": "AzureModelSettings", + "description": "Azure OpenAI model configuration (OpenAI-compatible)." + }, + "Base64Image": { + "properties": { + "type": { + "type": "string", + "const": "base64", + "title": "Type", + "description": "The source type for the image.", + "default": "base64" + }, + "media_type": { + "type": "string", + "title": "Media Type", + "description": "The media type for the image." + }, + "data": { + "type": "string", + "title": "Data", + "description": "The base64 encoded image data." + }, + "detail": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Detail", + "description": "What level of detail to use when processing and understanding the image (low, high, or auto to let the model decide)" + } + }, + "type": "object", + "required": ["media_type", "data"], + "title": "Base64Image" + }, + "BaseToolRuleSchema": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name" + }, + "type": { + "type": "string", + "title": "Type" + } + }, + "type": "object", + "required": ["tool_name", "type"], + "title": "BaseToolRuleSchema" + }, + "BasetenModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "baseten", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "baseten" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + } + }, + "type": "object", + "title": "BasetenModelSettings", + "description": "Baseten model configuration (OpenAI-compatible)." + }, + "BatchJob": { + "properties": { + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this object." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this object." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The unix timestamp of when the job was created." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the object was last updated." + }, + "status": { + "$ref": "#/components/schemas/JobStatus", + "description": "The status of the job.", + "default": "created" + }, + "completed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Completed At", + "description": "The unix timestamp of when the job was completed." + }, + "stop_reason": { + "anyOf": [ + { + "$ref": "#/components/schemas/StopReasonType" + }, + { + "type": "null" + } + ], + "description": "The reason why the job was stopped." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "The metadata of the job." + }, + "job_type": { + "$ref": "#/components/schemas/JobType", + "default": "batch" + }, + "background": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Background", + "description": "Whether the job was created in background mode." + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "The agent associated with this job/run." + }, + "callback_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Callback Url", + "description": "If set, POST to this URL when the job completes." + }, + "callback_sent_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Callback Sent At", + "description": "Timestamp when the callback was last attempted." + }, + "callback_status_code": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Callback Status Code", + "description": "HTTP status code returned by the callback endpoint." + }, + "callback_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Callback Error", + "description": "Optional error message from attempting to POST the callback endpoint." + }, + "ttft_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Ttft Ns", + "description": "Time to first token for a run in nanoseconds" + }, + "total_duration_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Total Duration Ns", + "description": "Total run duration in nanoseconds" + }, + "id": { + "type": "string", + "pattern": "^(job|run)-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Job", + "examples": ["job-123e4567-e89b-12d3-a456-426614174000"] + } + }, + "additionalProperties": false, + "type": "object", + "title": "BatchJob" + }, + "BedrockModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "bedrock", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "bedrock" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model." + } + }, + "type": "object", + "title": "BedrockModelSettings", + "description": "AWS Bedrock model configuration." + }, + "BillingContext": { + "properties": { + "plan_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Plan Type", + "description": "Subscription tier" + }, + "cost_source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cost Source", + "description": "Cost source: 'quota' or 'credits'" + }, + "customer_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Customer Id", + "description": "Customer ID for billing records" + } + }, + "type": "object", + "title": "BillingContext", + "description": "Billing context for LLM request cost tracking." + }, + "Block": { + "properties": { + "value": { + "type": "string", + "title": "Value", + "description": "Value of the block." + }, + "limit": { + "type": "integer", + "title": "Limit", + "description": "Character limit of the block.", + "default": 100000 + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "template_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Name", + "description": "Name of the block if it is a template." + }, + "is_template": { + "type": "boolean", + "title": "Is Template", + "description": "Whether the block is a template (e.g. saved human/persona options).", + "default": false + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template." + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "The base template id of the block." + }, + "deployment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deployment Id", + "description": "The id of the deployment." + }, + "entity_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Entity Id", + "description": "The id of the entity within the template." + }, + "preserve_on_migration": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Preserve On Migration", + "description": "Preserve the block on template migration.", + "default": false + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Label", + "description": "Label of the block (e.g. 'human', 'persona') in the context window." + }, + "read_only": { + "type": "boolean", + "title": "Read Only", + "description": "Whether the agent has read-only access to the block.", + "default": false + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of the block." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata of the block.", + "default": {} + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "If set to True, the block will be hidden." + }, + "id": { + "type": "string", + "pattern": "^block-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Block", + "examples": ["block-123e4567-e89b-12d3-a456-426614174000"] + }, + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this Block." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that last updated this Block." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "The tags associated with the block.", + "default": [] + } + }, + "type": "object", + "required": ["value"], + "title": "Block", + "description": "A Block represents a reserved section of the LLM's context window." + }, + "BlockResponse": { + "properties": { + "value": { + "type": "string", + "title": "Value", + "description": "Value of the block." + }, + "limit": { + "type": "integer", + "title": "Limit", + "description": "Character limit of the block.", + "default": 100000 + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "template_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Name", + "description": "(Deprecated) The name of the block template (if it is a template).", + "deprecated": true + }, + "is_template": { + "type": "boolean", + "title": "Is Template", + "description": "Whether the block is a template (e.g. saved human/persona options).", + "default": false + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "(Deprecated) The id of the template.", + "deprecated": true + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "(Deprecated) The base template id of the block.", + "deprecated": true + }, + "deployment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deployment Id", + "description": "(Deprecated) The id of the deployment.", + "deprecated": true + }, + "entity_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Entity Id", + "description": "(Deprecated) The id of the entity within the template.", + "deprecated": true + }, + "preserve_on_migration": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Preserve On Migration", + "description": "(Deprecated) Preserve the block on template migration.", + "default": false, + "deprecated": true + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Label", + "description": "Label of the block (e.g. 'human', 'persona') in the context window." + }, + "read_only": { + "type": "boolean", + "title": "Read Only", + "description": "(Deprecated) Whether the agent has read-only access to the block.", + "default": false, + "deprecated": true + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of the block." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata of the block.", + "default": {} + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "(Deprecated) If set to True, the block will be hidden.", + "deprecated": true + }, + "id": { + "type": "string", + "title": "Id", + "description": "The id of the block." + }, + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this Block." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that last updated this Block." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "The tags associated with the block.", + "default": [] + } + }, + "type": "object", + "required": ["value", "id"], + "title": "BlockResponse" + }, + "BlockSchema": { + "properties": { + "value": { + "type": "string", + "title": "Value", + "description": "Value of the block." + }, + "limit": { + "type": "integer", + "title": "Limit", + "description": "Character limit of the block.", + "default": 100000 + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "template_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Name", + "description": "Name of the block if it is a template." + }, + "is_template": { + "type": "boolean", + "title": "Is Template", + "default": false + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template." + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "The base template id of the block." + }, + "deployment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deployment Id", + "description": "The id of the deployment." + }, + "entity_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Entity Id", + "description": "The id of the entity within the template." + }, + "preserve_on_migration": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Preserve On Migration", + "description": "Preserve the block on template migration.", + "default": false + }, + "label": { + "type": "string", + "title": "Label", + "description": "Label of the block." + }, + "read_only": { + "type": "boolean", + "title": "Read Only", + "description": "Whether the agent has read-only access to the block.", + "default": false + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of the block." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata of the block.", + "default": {} + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "If set to True, the block will be hidden." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "The tags to associate with the block." + }, + "id": { + "type": "string", + "title": "Id", + "description": "Human-readable identifier for this block in the file" + } + }, + "type": "object", + "required": ["value", "label", "id"], + "title": "BlockSchema", + "description": "Block with human-readable ID for agent file" + }, + "BlockUpdate": { + "properties": { + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value", + "description": "Value of the block." + }, + "limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Limit", + "description": "Character limit of the block." + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "template_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Name", + "description": "Name of the block if it is a template." + }, + "is_template": { + "type": "boolean", + "title": "Is Template", + "description": "Whether the block is a template (e.g. saved human/persona options).", + "default": false + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template." + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "The base template id of the block." + }, + "deployment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deployment Id", + "description": "The id of the deployment." + }, + "entity_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Entity Id", + "description": "The id of the entity within the template." + }, + "preserve_on_migration": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Preserve On Migration", + "description": "Preserve the block on template migration.", + "default": false + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Label", + "description": "Label of the block (e.g. 'human', 'persona') in the context window." + }, + "read_only": { + "type": "boolean", + "title": "Read Only", + "description": "Whether the agent has read-only access to the block.", + "default": false + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of the block." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata of the block.", + "default": {} + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "If set to True, the block will be hidden." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "The tags to associate with the block." + } + }, + "type": "object", + "title": "BlockUpdate", + "description": "Update a block" + }, + "Body_export_agent": { + "properties": { + "spec": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentFileSchema" + }, + { + "type": "null" + } + ] + }, + "legacy_spec": { + "anyOf": [ + { + "$ref": "#/components/schemas/letta__serialize_schemas__pydantic_agent_schema__AgentSchema" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "title": "Body_export_agent" + }, + "Body_import_agent": { + "properties": { + "file": { + "type": "string", + "format": "binary", + "title": "File" + }, + "override_existing_tools": { + "type": "boolean", + "title": "Override Existing Tools", + "description": "If set to True, existing tools can get their source code overwritten by the uploaded tool definitions. Note that Letta core tools can never be updated externally.", + "default": true + }, + "strip_messages": { + "type": "boolean", + "title": "Strip Messages", + "description": "If set to True, strips all messages from the agent before importing.", + "default": false + }, + "secrets": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Secrets", + "description": "Secrets as a JSON string to pass to the agent for tool execution." + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "If provided, overrides the agent name with this value." + }, + "embedding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Embedding", + "description": "Embedding handle to override with." + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "Model handle to override the agent's default model. This allows the imported agent to use a different model while keeping other defaults (e.g., context size) from the original configuration." + }, + "append_copy_suffix": { + "type": "boolean", + "title": "Append Copy Suffix", + "description": "If set to True, appends \"_copy\" to the end of the agent name.", + "default": true, + "deprecated": true + }, + "override_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override Name", + "description": "If provided, overrides the agent name with this value. Use 'name' instead.", + "deprecated": true + }, + "override_embedding_handle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override Embedding Handle", + "description": "Override import with specific embedding handle. Use 'embedding' instead.", + "deprecated": true + }, + "override_model_handle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override Model Handle", + "description": "Model handle to override the agent's default model. Use 'model' instead.", + "deprecated": true + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The project ID to associate the uploaded agent with. This is now passed via headers.", + "deprecated": true + }, + "env_vars_json": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Env Vars Json", + "description": "Environment variables as a JSON string to pass to the agent for tool execution. Use 'secrets' instead.", + "deprecated": true + } + }, + "type": "object", + "required": ["file"], + "title": "Body_import_agent" + }, + "Body_upload_file_to_folder": { + "properties": { + "file": { + "type": "string", + "format": "binary", + "title": "File" + } + }, + "type": "object", + "required": ["file"], + "title": "Body_upload_file_to_folder" + }, + "Body_upload_file_to_source": { + "properties": { + "file": { + "type": "string", + "format": "binary", + "title": "File" + } + }, + "type": "object", + "required": ["file"], + "title": "Body_upload_file_to_source" + }, + "CancelAgentRunRequest": { + "properties": { + "run_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Run Ids", + "description": "Optional list of run IDs to cancel" + } + }, + "type": "object", + "title": "CancelAgentRunRequest" + }, + "ChatCompletion": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "choices": { + "items": { + "$ref": "#/components/schemas/Choice" + }, + "type": "array", + "title": "Choices" + }, + "created": { + "type": "integer", + "title": "Created" + }, + "model": { + "type": "string", + "title": "Model" + }, + "object": { + "type": "string", + "const": "chat.completion", + "title": "Object" + }, + "service_tier": { + "anyOf": [ + { + "type": "string", + "enum": ["auto", "default", "flex", "scale", "priority"] + }, + { + "type": "null" + } + ], + "title": "Service Tier" + }, + "system_fingerprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "System Fingerprint" + }, + "usage": { + "anyOf": [ + { + "$ref": "#/components/schemas/CompletionUsage" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": true, + "type": "object", + "required": ["id", "choices", "created", "model", "object"], + "title": "ChatCompletion", + "description": "Represents a chat completion response returned by model, based on the provided input." + }, + "ChatCompletionAssistantMessageParam": { + "properties": { + "role": { + "type": "string", + "const": "assistant", + "title": "Role" + }, + "audio": { + "anyOf": [ + { + "$ref": "#/components/schemas/Audio" + }, + { + "type": "null" + } + ] + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/ChatCompletionContentPartTextParam" + }, + { + "$ref": "#/components/schemas/ChatCompletionContentPartRefusalParam" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Content" + }, + "function_call": { + "anyOf": [ + { + "$ref": "#/components/schemas/FunctionCall-Input" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string", + "title": "Name" + }, + "refusal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Refusal" + }, + "tool_calls": { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/ChatCompletionMessageFunctionToolCallParam" + }, + { + "$ref": "#/components/schemas/ChatCompletionMessageCustomToolCallParam" + } + ] + }, + "type": "array", + "title": "Tool Calls" + } + }, + "type": "object", + "required": ["role"], + "title": "ChatCompletionAssistantMessageParam", + "description": "Messages sent by the model in response to user messages." + }, + "ChatCompletionAudio": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "data": { + "type": "string", + "title": "Data" + }, + "expires_at": { + "type": "integer", + "title": "Expires At" + }, + "transcript": { + "type": "string", + "title": "Transcript" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["id", "data", "expires_at", "transcript"], + "title": "ChatCompletionAudio", + "description": "If the audio output modality is requested, this object contains data\nabout the audio response from the model. [Learn more](https://platform.openai.com/docs/guides/audio)." + }, + "ChatCompletionContentPartImageParam": { + "properties": { + "image_url": { + "$ref": "#/components/schemas/ImageURL" + }, + "type": { + "type": "string", + "const": "image_url", + "title": "Type" + } + }, + "type": "object", + "required": ["image_url", "type"], + "title": "ChatCompletionContentPartImageParam", + "description": "Learn about [image inputs](https://platform.openai.com/docs/guides/vision)." + }, + "ChatCompletionContentPartInputAudioParam": { + "properties": { + "input_audio": { + "$ref": "#/components/schemas/InputAudio" + }, + "type": { + "type": "string", + "const": "input_audio", + "title": "Type" + } + }, + "type": "object", + "required": ["input_audio", "type"], + "title": "ChatCompletionContentPartInputAudioParam", + "description": "Learn about [audio inputs](https://platform.openai.com/docs/guides/audio)." + }, + "ChatCompletionContentPartRefusalParam": { + "properties": { + "refusal": { + "type": "string", + "title": "Refusal" + }, + "type": { + "type": "string", + "const": "refusal", + "title": "Type" + } + }, + "type": "object", + "required": ["refusal", "type"], + "title": "ChatCompletionContentPartRefusalParam" + }, + "ChatCompletionContentPartTextParam": { + "properties": { + "text": { + "type": "string", + "title": "Text" + }, + "type": { + "type": "string", + "const": "text", + "title": "Type" + } + }, + "type": "object", + "required": ["text", "type"], + "title": "ChatCompletionContentPartTextParam", + "description": "Learn about [text inputs](https://platform.openai.com/docs/guides/text-generation)." + }, + "ChatCompletionDeveloperMessageParam": { + "properties": { + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "$ref": "#/components/schemas/ChatCompletionContentPartTextParam" + }, + "type": "array" + } + ], + "title": "Content" + }, + "role": { + "type": "string", + "const": "developer", + "title": "Role" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": ["content", "role"], + "title": "ChatCompletionDeveloperMessageParam", + "description": "Developer-provided instructions that the model should follow, regardless of\nmessages sent by the user. With o1 models and newer, `developer` messages\nreplace the previous `system` messages." + }, + "ChatCompletionFunctionMessageParam": { + "properties": { + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Content" + }, + "name": { + "type": "string", + "title": "Name" + }, + "role": { + "type": "string", + "const": "function", + "title": "Role" + } + }, + "type": "object", + "required": ["content", "name", "role"], + "title": "ChatCompletionFunctionMessageParam" + }, + "ChatCompletionMessage": { + "properties": { + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Content" + }, + "refusal": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Refusal" + }, + "role": { + "type": "string", + "const": "assistant", + "title": "Role" + }, + "annotations": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Annotation" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Annotations" + }, + "audio": { + "anyOf": [ + { + "$ref": "#/components/schemas/ChatCompletionAudio" + }, + { + "type": "null" + } + ] + }, + "function_call": { + "anyOf": [ + { + "$ref": "#/components/schemas/FunctionCall-Output" + }, + { + "type": "null" + } + ] + }, + "tool_calls": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/ChatCompletionMessageFunctionToolCall-Output" + }, + { + "$ref": "#/components/schemas/ChatCompletionMessageCustomToolCall" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Calls" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["role"], + "title": "ChatCompletionMessage", + "description": "A chat completion message generated by the model." + }, + "ChatCompletionMessageCustomToolCall": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "custom": { + "$ref": "#/components/schemas/Custom-Output" + }, + "type": { + "type": "string", + "const": "custom", + "title": "Type" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["id", "custom", "type"], + "title": "ChatCompletionMessageCustomToolCall", + "description": "A call to a custom tool created by the model." + }, + "ChatCompletionMessageCustomToolCallParam": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "custom": { + "$ref": "#/components/schemas/Custom-Input" + }, + "type": { + "type": "string", + "const": "custom", + "title": "Type" + } + }, + "type": "object", + "required": ["id", "custom", "type"], + "title": "ChatCompletionMessageCustomToolCallParam", + "description": "A call to a custom tool created by the model." + }, + "ChatCompletionMessageFunctionToolCall-Input": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "function": { + "$ref": "#/components/schemas/openai__types__chat__chat_completion_message_function_tool_call__Function" + }, + "type": { + "type": "string", + "const": "function", + "title": "Type" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["id", "function", "type"], + "title": "ChatCompletionMessageFunctionToolCall", + "description": "A call to a function tool created by the model." + }, + "ChatCompletionMessageFunctionToolCall-Output": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "function": { + "$ref": "#/components/schemas/Function-Output" + }, + "type": { + "type": "string", + "const": "function", + "title": "Type" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["id", "function", "type"], + "title": "ChatCompletionMessageFunctionToolCall", + "description": "A call to a function tool created by the model." + }, + "ChatCompletionMessageFunctionToolCallParam": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "function": { + "$ref": "#/components/schemas/openai__types__chat__chat_completion_message_function_tool_call_param__Function" + }, + "type": { + "type": "string", + "const": "function", + "title": "Type" + } + }, + "type": "object", + "required": ["id", "function", "type"], + "title": "ChatCompletionMessageFunctionToolCallParam", + "description": "A call to a function tool created by the model." + }, + "ChatCompletionRequest": { + "properties": { + "model": { + "type": "string", + "title": "Model", + "description": "ID of the model to use" + }, + "messages": { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/ChatCompletionDeveloperMessageParam" + }, + { + "$ref": "#/components/schemas/ChatCompletionSystemMessageParam" + }, + { + "$ref": "#/components/schemas/ChatCompletionUserMessageParam" + }, + { + "$ref": "#/components/schemas/ChatCompletionAssistantMessageParam" + }, + { + "$ref": "#/components/schemas/ChatCompletionToolMessageParam" + }, + { + "$ref": "#/components/schemas/ChatCompletionFunctionMessageParam" + } + ] + }, + "type": "array", + "title": "Messages", + "description": "Messages comprising the conversation so far" + }, + "temperature": { + "anyOf": [ + { + "type": "number", + "maximum": 2, + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Temperature", + "description": "Sampling temperature" + }, + "top_p": { + "anyOf": [ + { + "type": "number", + "maximum": 1, + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Top P", + "description": "Nucleus sampling parameter" + }, + "n": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ], + "title": "N", + "description": "Number of chat completion choices to generate", + "default": 1 + }, + "stream": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Stream", + "description": "Whether to stream back partial progress", + "default": false + }, + "stop": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Stop", + "description": "Sequences where the API will stop generating" + }, + "max_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Tokens", + "description": "Maximum number of tokens to generate" + }, + "presence_penalty": { + "anyOf": [ + { + "type": "number", + "maximum": 2, + "minimum": -2 + }, + { + "type": "null" + } + ], + "title": "Presence Penalty", + "description": "Presence penalty" + }, + "frequency_penalty": { + "anyOf": [ + { + "type": "number", + "maximum": 2, + "minimum": -2 + }, + { + "type": "null" + } + ], + "title": "Frequency Penalty", + "description": "Frequency penalty" + }, + "user": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User", + "description": "A unique identifier representing your end-user" + } + }, + "type": "object", + "required": ["model", "messages"], + "title": "ChatCompletionRequest", + "description": "OpenAI-compatible chat completion request - exactly matching OpenAI's schema." + }, + "ChatCompletionSystemMessageParam": { + "properties": { + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "$ref": "#/components/schemas/ChatCompletionContentPartTextParam" + }, + "type": "array" + } + ], + "title": "Content" + }, + "role": { + "type": "string", + "const": "system", + "title": "Role" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": ["content", "role"], + "title": "ChatCompletionSystemMessageParam", + "description": "Developer-provided instructions that the model should follow, regardless of\nmessages sent by the user. With o1 models and newer, use `developer` messages\nfor this purpose instead." + }, + "ChatCompletionToolMessageParam": { + "properties": { + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "$ref": "#/components/schemas/ChatCompletionContentPartTextParam" + }, + "type": "array" + } + ], + "title": "Content" + }, + "role": { + "type": "string", + "const": "tool", + "title": "Role" + }, + "tool_call_id": { + "type": "string", + "title": "Tool Call Id" + } + }, + "type": "object", + "required": ["content", "role", "tool_call_id"], + "title": "ChatCompletionToolMessageParam" + }, + "ChatCompletionUserMessageParam": { + "properties": { + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/ChatCompletionContentPartTextParam" + }, + { + "$ref": "#/components/schemas/ChatCompletionContentPartImageParam" + }, + { + "$ref": "#/components/schemas/ChatCompletionContentPartInputAudioParam" + }, + { + "$ref": "#/components/schemas/File" + } + ] + }, + "type": "array" + } + ], + "title": "Content" + }, + "role": { + "type": "string", + "const": "user", + "title": "Role" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": ["content", "role"], + "title": "ChatCompletionUserMessageParam", + "description": "Messages sent by an end user, containing prompts or additional context\ninformation." + }, + "ChatGPTOAuthModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "chatgpt_oauth", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "chatgpt_oauth" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "reasoning": { + "$ref": "#/components/schemas/ChatGPTOAuthReasoning", + "description": "The reasoning configuration for the model.", + "default": { + "reasoning_effort": "medium" + } + } + }, + "type": "object", + "title": "ChatGPTOAuthModelSettings", + "description": "ChatGPT OAuth model configuration (uses ChatGPT backend API)." + }, + "ChatGPTOAuthReasoning": { + "properties": { + "reasoning_effort": { + "type": "string", + "enum": ["none", "low", "medium", "high", "xhigh"], + "title": "Reasoning Effort", + "description": "The reasoning effort level for GPT-5.x and o-series models.", + "default": "medium" + } + }, + "type": "object", + "title": "ChatGPTOAuthReasoning", + "description": "Reasoning configuration for ChatGPT OAuth models (GPT-5.x, o-series)." + }, + "ChildToolRule": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name", + "description": "The name of the tool. Must exist in the database for the user's organization." + }, + "type": { + "type": "string", + "const": "constrain_child_tools", + "title": "Type", + "default": "constrain_child_tools" + }, + "prompt_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt Template", + "description": "Optional template string (ignored)." + }, + "children": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Children", + "description": "The children tools that can be invoked." + }, + "child_arg_nodes": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ToolCallNode" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Child Arg Nodes", + "description": "Optional list of typed child argument overrides. Each node must reference a child in 'children'." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["tool_name", "children"], + "title": "ChildToolRule", + "description": "A ToolRule represents a tool that can be invoked by the agent." + }, + "ChildToolRuleSchema": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name" + }, + "type": { + "type": "string", + "title": "Type" + }, + "children": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Children" + } + }, + "type": "object", + "required": ["tool_name", "type", "children"], + "title": "ChildToolRuleSchema" + }, + "Choice": { + "properties": { + "finish_reason": { + "type": "string", + "enum": [ + "stop", + "length", + "tool_calls", + "content_filter", + "function_call" + ], + "title": "Finish Reason" + }, + "index": { + "type": "integer", + "title": "Index" + }, + "logprobs": { + "anyOf": [ + { + "$ref": "#/components/schemas/openai__types__chat__chat_completion__ChoiceLogprobs" + }, + { + "type": "null" + } + ] + }, + "message": { + "$ref": "#/components/schemas/ChatCompletionMessage" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["finish_reason", "index", "message"], + "title": "Choice" + }, + "ClientSkillSchema": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the skill" + }, + "description": { + "type": "string", + "title": "Description", + "description": "Description of what the skill does" + }, + "location": { + "type": "string", + "title": "Location", + "description": "Path or location hint for the skill (e.g. skills/my-skill/SKILL.md)" + } + }, + "type": "object", + "required": ["name", "description", "location"], + "title": "ClientSkillSchema", + "description": "Schema for a client-side skill passed in the request.\n\nClient-side skills represent environment-provided capabilities (e.g. project-scoped\nskills) that are not stored in the agent's MemFS but should appear in the system\nprompt's available skills section." + }, + "ClientToolSchema": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the tool function" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of what the tool does" + }, + "parameters": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Parameters", + "description": "JSON Schema for the function parameters" + } + }, + "type": "object", + "required": ["name"], + "title": "ClientToolSchema", + "description": "Schema for a client-side tool passed in the request.\n\nClient-side tools are executed by the client, not the server. When the agent\ncalls a client-side tool, execution pauses and returns control to the client\nto execute the tool and provide the result." + }, + "CodeInput": { + "properties": { + "code": { + "type": "string", + "title": "Code", + "description": "Source code to parse for JSON schema" + }, + "source_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Type", + "description": "The source type of the code (python or typescript)", + "default": "python" + } + }, + "type": "object", + "required": ["code"], + "title": "CodeInput" + }, + "CompactionResponse": { + "properties": { + "summary": { + "type": "string", + "title": "Summary" + }, + "num_messages_before": { + "type": "integer", + "title": "Num Messages Before" + }, + "num_messages_after": { + "type": "integer", + "title": "Num Messages After" + } + }, + "type": "object", + "required": ["summary", "num_messages_before", "num_messages_after"], + "title": "CompactionResponse" + }, + "CompactionSettings-Input": { + "properties": { + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "Model handle to use for sliding_window/all summarization (format: provider/model-name). If None, uses lightweight provider-specific defaults." + }, + "model_settings": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/OpenAIModelSettings" + }, + { + "$ref": "#/components/schemas/SGLangModelSettings" + }, + { + "$ref": "#/components/schemas/AnthropicModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleAIModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleVertexModelSettings" + }, + { + "$ref": "#/components/schemas/AzureModelSettings" + }, + { + "$ref": "#/components/schemas/XAIModelSettings" + }, + { + "$ref": "#/components/schemas/ZAIModelSettings" + }, + { + "$ref": "#/components/schemas/GroqModelSettings" + }, + { + "$ref": "#/components/schemas/DeepseekModelSettings" + }, + { + "$ref": "#/components/schemas/TogetherModelSettings" + }, + { + "$ref": "#/components/schemas/BedrockModelSettings" + }, + { + "$ref": "#/components/schemas/BasetenModelSettings" + }, + { + "$ref": "#/components/schemas/OpenRouterModelSettings" + }, + { + "$ref": "#/components/schemas/ChatGPTOAuthModelSettings" + } + ], + "discriminator": { + "propertyName": "provider_type", + "mapping": { + "anthropic": "#/components/schemas/AnthropicModelSettings", + "azure": "#/components/schemas/AzureModelSettings", + "baseten": "#/components/schemas/BasetenModelSettings", + "bedrock": "#/components/schemas/BedrockModelSettings", + "chatgpt_oauth": "#/components/schemas/ChatGPTOAuthModelSettings", + "deepseek": "#/components/schemas/DeepseekModelSettings", + "google_ai": "#/components/schemas/GoogleAIModelSettings", + "google_vertex": "#/components/schemas/GoogleVertexModelSettings", + "groq": "#/components/schemas/GroqModelSettings", + "openai": "#/components/schemas/OpenAIModelSettings", + "openrouter": "#/components/schemas/OpenRouterModelSettings", + "sglang": "#/components/schemas/SGLangModelSettings", + "together": "#/components/schemas/TogetherModelSettings", + "xai": "#/components/schemas/XAIModelSettings", + "zai": "#/components/schemas/ZAIModelSettings" + } + } + }, + { + "type": "null" + } + ], + "title": "Model Settings", + "description": "Optional model settings used to override defaults for the summarizer model." + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt", + "description": "The prompt to use for summarization. If None, uses mode-specific default." + }, + "prompt_acknowledgement": { + "type": "boolean", + "title": "Prompt Acknowledgement", + "description": "Whether to include an acknowledgement post-prompt (helps prevent non-summary outputs).", + "default": false + }, + "clip_chars": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Clip Chars", + "description": "The maximum length of the summary in characters. If none, no clipping is performed.", + "default": 50000 + }, + "mode": { + "type": "string", + "enum": [ + "all", + "sliding_window", + "self_compact_all", + "self_compact_sliding_window" + ], + "title": "Mode", + "description": "The type of summarization technique use.", + "default": "sliding_window" + }, + "sliding_window_percentage": { + "type": "number", + "title": "Sliding Window Percentage", + "description": "The percentage of the context window to keep post-summarization (only used in sliding window modes)." + } + }, + "type": "object", + "title": "CompactionSettings", + "description": "Configuration for conversation compaction / summarization.\n\nPer-model settings (temperature,\nmax tokens, etc.) are derived from the default configuration for that handle." + }, + "CompactionSettings-Output": { + "properties": { + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "Model handle to use for sliding_window/all summarization (format: provider/model-name). If None, uses lightweight provider-specific defaults." + }, + "model_settings": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/OpenAIModelSettings" + }, + { + "$ref": "#/components/schemas/SGLangModelSettings" + }, + { + "$ref": "#/components/schemas/AnthropicModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleAIModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleVertexModelSettings" + }, + { + "$ref": "#/components/schemas/AzureModelSettings" + }, + { + "$ref": "#/components/schemas/XAIModelSettings" + }, + { + "$ref": "#/components/schemas/ZAIModelSettings" + }, + { + "$ref": "#/components/schemas/GroqModelSettings" + }, + { + "$ref": "#/components/schemas/DeepseekModelSettings" + }, + { + "$ref": "#/components/schemas/TogetherModelSettings" + }, + { + "$ref": "#/components/schemas/BedrockModelSettings" + }, + { + "$ref": "#/components/schemas/BasetenModelSettings" + }, + { + "$ref": "#/components/schemas/OpenRouterModelSettings" + }, + { + "$ref": "#/components/schemas/ChatGPTOAuthModelSettings" + } + ], + "discriminator": { + "propertyName": "provider_type", + "mapping": { + "anthropic": "#/components/schemas/AnthropicModelSettings", + "azure": "#/components/schemas/AzureModelSettings", + "baseten": "#/components/schemas/BasetenModelSettings", + "bedrock": "#/components/schemas/BedrockModelSettings", + "chatgpt_oauth": "#/components/schemas/ChatGPTOAuthModelSettings", + "deepseek": "#/components/schemas/DeepseekModelSettings", + "google_ai": "#/components/schemas/GoogleAIModelSettings", + "google_vertex": "#/components/schemas/GoogleVertexModelSettings", + "groq": "#/components/schemas/GroqModelSettings", + "openai": "#/components/schemas/OpenAIModelSettings", + "openrouter": "#/components/schemas/OpenRouterModelSettings", + "sglang": "#/components/schemas/SGLangModelSettings", + "together": "#/components/schemas/TogetherModelSettings", + "xai": "#/components/schemas/XAIModelSettings", + "zai": "#/components/schemas/ZAIModelSettings" + } + } + }, + { + "type": "null" + } + ], + "title": "Model Settings", + "description": "Optional model settings used to override defaults for the summarizer model." + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt", + "description": "The prompt to use for summarization. If None, uses mode-specific default." + }, + "prompt_acknowledgement": { + "type": "boolean", + "title": "Prompt Acknowledgement", + "description": "Whether to include an acknowledgement post-prompt (helps prevent non-summary outputs).", + "default": false + }, + "clip_chars": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Clip Chars", + "description": "The maximum length of the summary in characters. If none, no clipping is performed.", + "default": 50000 + }, + "mode": { + "type": "string", + "enum": [ + "all", + "sliding_window", + "self_compact_all", + "self_compact_sliding_window" + ], + "title": "Mode", + "description": "The type of summarization technique use.", + "default": "sliding_window" + }, + "sliding_window_percentage": { + "type": "number", + "title": "Sliding Window Percentage", + "description": "The percentage of the context window to keep post-summarization (only used in sliding window modes)." + } + }, + "type": "object", + "title": "CompactionSettings", + "description": "Configuration for conversation compaction / summarization.\n\nPer-model settings (temperature,\nmax tokens, etc.) are derived from the default configuration for that handle." + }, + "CompactionStats": { + "properties": { + "trigger": { + "type": "string", + "title": "Trigger", + "description": "What triggered the compaction (e.g., 'context_window_exceeded', 'post_step_context_check')" + }, + "context_tokens_before": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Context Tokens Before", + "description": "Token count before compaction (from LLM usage stats, includes full context sent to LLM)" + }, + "context_tokens_after": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Context Tokens After", + "description": "Token count after compaction (message tokens only, does not include tool definitions)" + }, + "context_window": { + "type": "integer", + "title": "Context Window", + "description": "The model's context window size" + }, + "messages_count_before": { + "type": "integer", + "title": "Messages Count Before", + "description": "Number of messages before compaction" + }, + "messages_count_after": { + "type": "integer", + "title": "Messages Count After", + "description": "Number of messages after compaction" + } + }, + "type": "object", + "required": [ + "trigger", + "context_window", + "messages_count_before", + "messages_count_after" + ], + "title": "CompactionStats", + "description": "Statistics about a memory compaction operation." + }, + "ComparisonOperator": { + "type": "string", + "enum": ["eq", "gte", "lte"], + "title": "ComparisonOperator", + "description": "Comparison operators for filtering numeric values" + }, + "CompletionTokensDetails": { + "properties": { + "accepted_prediction_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Accepted Prediction Tokens" + }, + "audio_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Audio Tokens" + }, + "reasoning_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Reasoning Tokens" + }, + "rejected_prediction_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Rejected Prediction Tokens" + } + }, + "additionalProperties": true, + "type": "object", + "title": "CompletionTokensDetails", + "description": "Breakdown of tokens used in a completion." + }, + "CompletionUsage": { + "properties": { + "completion_tokens": { + "type": "integer", + "title": "Completion Tokens" + }, + "prompt_tokens": { + "type": "integer", + "title": "Prompt Tokens" + }, + "total_tokens": { + "type": "integer", + "title": "Total Tokens" + }, + "completion_tokens_details": { + "anyOf": [ + { + "$ref": "#/components/schemas/CompletionTokensDetails" + }, + { + "type": "null" + } + ] + }, + "prompt_tokens_details": { + "anyOf": [ + { + "$ref": "#/components/schemas/PromptTokensDetails" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": true, + "type": "object", + "required": ["completion_tokens", "prompt_tokens", "total_tokens"], + "title": "CompletionUsage", + "description": "Usage statistics for the completion request." + }, + "ConditionalToolRule": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name", + "description": "The name of the tool. Must exist in the database for the user's organization." + }, + "type": { + "type": "string", + "const": "conditional", + "title": "Type", + "default": "conditional" + }, + "prompt_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt Template", + "description": "Optional template string (ignored)." + }, + "default_child": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Default Child", + "description": "The default child tool to be called. If None, any tool can be called." + }, + "child_output_mapping": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Child Output Mapping", + "description": "The output case to check for mapping" + }, + "require_output_mapping": { + "type": "boolean", + "title": "Require Output Mapping", + "description": "Whether to throw an error when output doesn't match any case", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "required": ["tool_name", "child_output_mapping"], + "title": "ConditionalToolRule", + "description": "A ToolRule that conditionally maps to different child tools based on the output." + }, + "ConditionalToolRuleSchema": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name" + }, + "type": { + "type": "string", + "title": "Type" + }, + "default_child": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Default Child" + }, + "child_output_mapping": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Child Output Mapping" + }, + "require_output_mapping": { + "type": "boolean", + "title": "Require Output Mapping" + } + }, + "type": "object", + "required": [ + "tool_name", + "type", + "default_child", + "child_output_mapping", + "require_output_mapping" + ], + "title": "ConditionalToolRuleSchema" + }, + "ContextWindowOverview": { + "properties": { + "context_window_size_max": { + "type": "integer", + "title": "Context Window Size Max", + "description": "The maximum amount of tokens the context window can hold." + }, + "context_window_size_current": { + "type": "integer", + "title": "Context Window Size Current", + "description": "The current number of tokens in the context window." + }, + "num_messages": { + "type": "integer", + "title": "Num Messages", + "description": "The number of messages in the context window." + }, + "num_archival_memory": { + "type": "integer", + "title": "Num Archival Memory", + "description": "The number of messages in the archival memory." + }, + "num_recall_memory": { + "type": "integer", + "title": "Num Recall Memory", + "description": "The number of messages in the recall memory." + }, + "num_tokens_external_memory_summary": { + "type": "integer", + "title": "Num Tokens External Memory Summary", + "description": "The number of tokens in the external memory summary (archival + recall metadata)." + }, + "external_memory_summary": { + "type": "string", + "title": "External Memory Summary", + "description": "The metadata summary of the external memory sources (archival + recall metadata)." + }, + "num_tokens_system": { + "type": "integer", + "title": "Num Tokens System", + "description": "The number of tokens in the system prompt." + }, + "system_prompt": { + "type": "string", + "title": "System Prompt", + "description": "The content of the system prompt." + }, + "num_tokens_core_memory": { + "type": "integer", + "title": "Num Tokens Core Memory", + "description": "The number of tokens in the core memory." + }, + "core_memory": { + "type": "string", + "title": "Core Memory", + "description": "The content of the core memory." + }, + "num_tokens_memory_filesystem": { + "type": "integer", + "title": "Num Tokens Memory Filesystem", + "description": "The number of tokens in the memory filesystem section (git-enabled agents only).", + "default": 0 + }, + "memory_filesystem": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Memory Filesystem", + "description": "The content of the memory filesystem section." + }, + "num_tokens_tool_usage_rules": { + "type": "integer", + "title": "Num Tokens Tool Usage Rules", + "description": "The number of tokens in the tool usage rules section.", + "default": 0 + }, + "tool_usage_rules": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tool Usage Rules", + "description": "The content of the tool usage rules section." + }, + "num_tokens_directories": { + "type": "integer", + "title": "Num Tokens Directories", + "description": "The number of tokens in the directories section (attached sources).", + "default": 0 + }, + "directories": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Directories", + "description": "The content of the directories section." + }, + "num_tokens_summary_memory": { + "type": "integer", + "title": "Num Tokens Summary Memory", + "description": "The number of tokens in the summary memory." + }, + "summary_memory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Summary Memory", + "description": "The content of the summary memory." + }, + "num_tokens_functions_definitions": { + "type": "integer", + "title": "Num Tokens Functions Definitions", + "description": "The number of tokens in the functions definitions." + }, + "functions_definitions": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/FunctionTool" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Functions Definitions", + "description": "The content of the functions definitions." + }, + "num_tokens_messages": { + "type": "integer", + "title": "Num Tokens Messages", + "description": "The number of tokens in the messages list." + }, + "messages": { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array", + "title": "Messages", + "description": "The messages in the context window." + } + }, + "type": "object", + "required": [ + "context_window_size_max", + "context_window_size_current", + "num_messages", + "num_archival_memory", + "num_recall_memory", + "num_tokens_external_memory_summary", + "external_memory_summary", + "num_tokens_system", + "system_prompt", + "num_tokens_core_memory", + "core_memory", + "num_tokens_summary_memory", + "num_tokens_functions_definitions", + "functions_definitions", + "num_tokens_messages", + "messages" + ], + "title": "ContextWindowOverview", + "description": "Overview of the context window, including the number of messages and tokens." + }, + "ContinueToolRule": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name", + "description": "The name of the tool. Must exist in the database for the user's organization." + }, + "type": { + "type": "string", + "const": "continue_loop", + "title": "Type", + "default": "continue_loop" + }, + "prompt_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt Template", + "description": "Optional template string (ignored)." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["tool_name"], + "title": "ContinueToolRule", + "description": "Represents a tool rule configuration where if this tool gets called, it must continue the agent loop." + }, + "Conversation": { + "properties": { + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this object." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this object." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "The timestamp when the object was created." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the object was last updated." + }, + "id": { + "type": "string", + "title": "Id", + "description": "The unique identifier of the conversation." + }, + "agent_id": { + "type": "string", + "title": "Agent Id", + "description": "The ID of the agent this conversation belongs to." + }, + "summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Summary", + "description": "A summary of the conversation." + }, + "in_context_message_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "In Context Message Ids", + "description": "The IDs of in-context messages for the conversation." + }, + "isolated_block_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Isolated Block Ids", + "description": "IDs of blocks that are isolated (specific to this conversation, overriding agent defaults)." + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "The model handle for this conversation (overrides agent's model). Format: provider/model-name." + }, + "model_settings": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/OpenAIModelSettings" + }, + { + "$ref": "#/components/schemas/SGLangModelSettings" + }, + { + "$ref": "#/components/schemas/AnthropicModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleAIModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleVertexModelSettings" + }, + { + "$ref": "#/components/schemas/AzureModelSettings" + }, + { + "$ref": "#/components/schemas/XAIModelSettings" + }, + { + "$ref": "#/components/schemas/ZAIModelSettings" + }, + { + "$ref": "#/components/schemas/GroqModelSettings" + }, + { + "$ref": "#/components/schemas/DeepseekModelSettings" + }, + { + "$ref": "#/components/schemas/TogetherModelSettings" + }, + { + "$ref": "#/components/schemas/BedrockModelSettings" + }, + { + "$ref": "#/components/schemas/BasetenModelSettings" + }, + { + "$ref": "#/components/schemas/OpenRouterModelSettings" + }, + { + "$ref": "#/components/schemas/ChatGPTOAuthModelSettings" + } + ], + "discriminator": { + "propertyName": "provider_type", + "mapping": { + "anthropic": "#/components/schemas/AnthropicModelSettings", + "azure": "#/components/schemas/AzureModelSettings", + "baseten": "#/components/schemas/BasetenModelSettings", + "bedrock": "#/components/schemas/BedrockModelSettings", + "chatgpt_oauth": "#/components/schemas/ChatGPTOAuthModelSettings", + "deepseek": "#/components/schemas/DeepseekModelSettings", + "google_ai": "#/components/schemas/GoogleAIModelSettings", + "google_vertex": "#/components/schemas/GoogleVertexModelSettings", + "groq": "#/components/schemas/GroqModelSettings", + "openai": "#/components/schemas/OpenAIModelSettings", + "openrouter": "#/components/schemas/OpenRouterModelSettings", + "sglang": "#/components/schemas/SGLangModelSettings", + "together": "#/components/schemas/TogetherModelSettings", + "xai": "#/components/schemas/XAIModelSettings", + "zai": "#/components/schemas/ZAIModelSettings" + } + } + }, + { + "type": "null" + } + ], + "title": "Model Settings", + "description": "The model settings for this conversation (overrides agent's model settings)." + }, + "last_message_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Message At", + "description": "Timestamp of the most recent message request sent to this conversation." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["id", "agent_id"], + "title": "Conversation", + "description": "Represents a conversation on an agent for concurrent messaging." + }, + "ConversationMessageRequest": { + "properties": { + "messages": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageCreate" + }, + { + "$ref": "#/components/schemas/ApprovalCreate" + }, + { + "$ref": "#/components/schemas/ToolReturnCreate" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Messages", + "description": "The messages to be sent to the agent." + }, + "input": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "$ref": "#/components/schemas/ToolCallContent" + }, + { + "$ref": "#/components/schemas/ToolReturnContent" + }, + { + "$ref": "#/components/schemas/ReasoningContent" + }, + { + "$ref": "#/components/schemas/RedactedReasoningContent" + }, + { + "$ref": "#/components/schemas/OmittedReasoningContent" + }, + { + "$ref": "#/components/schemas/SummarizedReasoningContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "image": "#/components/schemas/ImageContent", + "omitted_reasoning": "#/components/schemas/OmittedReasoningContent", + "reasoning": "#/components/schemas/ReasoningContent", + "redacted_reasoning": "#/components/schemas/RedactedReasoningContent", + "summarized_reasoning": "#/components/schemas/SummarizedReasoningContent", + "text": "#/components/schemas/TextContent", + "tool_call": "#/components/schemas/ToolCallContent", + "tool_return": "#/components/schemas/ToolReturnContent" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Input", + "description": "Syntactic sugar for a single user message. Equivalent to messages=[{'role': 'user', 'content': input}]." + }, + "max_steps": { + "type": "integer", + "title": "Max Steps", + "description": "Maximum number of steps the agent should take to process the request.", + "default": 50 + }, + "use_assistant_message": { + "type": "boolean", + "title": "Use Assistant Message", + "description": "Whether the server should parse specific tool call arguments (default `send_message`) as `AssistantMessage` objects. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": true, + "deprecated": true + }, + "assistant_message_tool_name": { + "type": "string", + "title": "Assistant Message Tool Name", + "description": "The name of the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": "send_message", + "deprecated": true + }, + "assistant_message_tool_kwarg": { + "type": "string", + "title": "Assistant Message Tool Kwarg", + "description": "The name of the message argument in the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": "message", + "deprecated": true + }, + "include_return_message_types": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MessageType" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Include Return Message Types", + "description": "Only return specified message types in the response. If `None` (default) returns all messages." + }, + "enable_thinking": { + "type": "string", + "title": "Enable Thinking", + "description": "If set to True, enables reasoning before responses or tool calls from the agent.", + "default": true, + "deprecated": true + }, + "client_tools": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ClientToolSchema" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Client Tools", + "description": "Client-side tools that the agent can call. When the agent calls a client-side tool, execution pauses and returns control to the client to execute the tool and provide the result via a ToolReturn." + }, + "client_skills": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ClientSkillSchema" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Client Skills", + "description": "Client-side skills available in the environment. These are rendered in the system prompt's available skills section alongside agent-scoped skills from MemFS." + }, + "override_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override Model", + "description": "Model handle to use for this request instead of the agent's default model. This allows sending a message to a different model without changing the agent's configuration." + }, + "include_compaction_messages": { + "type": "boolean", + "title": "Include Compaction Messages", + "description": "If True, compaction events emit structured `SummaryMessage` and `EventMessage` types. If False (default), compaction messages are not included in the response.", + "default": false + }, + "return_logprobs": { + "type": "boolean", + "title": "Return Logprobs", + "description": "If True, returns log probabilities of the output tokens in the response. Useful for RL training. Only supported for OpenAI-compatible providers (including SGLang).", + "default": false + }, + "top_logprobs": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Top Logprobs", + "description": "Number of most likely tokens to return at each position (0-20). Requires return_logprobs=True." + }, + "return_token_ids": { + "type": "boolean", + "title": "Return Token Ids", + "description": "If True, returns token IDs and logprobs for ALL LLM generations in the agent step, not just the last one. Uses SGLang native /generate endpoint. Returns 'turns' field with TurnTokenData for each assistant/tool turn. Required for proper multi-turn RL training with loss masking.", + "default": false + }, + "override_system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override System", + "description": "Optional per-request system prompt override. When set, this is passed directly to the underlying LLM request and bypasses the persisted/compiled system message for that request." + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "Agent ID for agent-direct mode with 'default' conversation. Use with conversation_id='default' in the URL path." + }, + "streaming": { + "type": "boolean", + "title": "Streaming", + "description": "If True (default), returns a streaming response (Server-Sent Events). If False, returns a complete JSON response.", + "default": true + }, + "stream_tokens": { + "type": "boolean", + "title": "Stream Tokens", + "description": "Flag to determine if individual tokens should be streamed, rather than streaming per step (only used when streaming=true).", + "default": false + }, + "include_pings": { + "type": "boolean", + "title": "Include Pings", + "description": "Whether to include periodic keepalive ping messages in the stream to prevent connection timeouts (only used when streaming=true).", + "default": true + }, + "background": { + "type": "boolean", + "title": "Background", + "description": "Whether to process the request in the background (only used when streaming=true).", + "default": false + } + }, + "type": "object", + "title": "ConversationMessageRequest", + "description": "Request for sending messages to a conversation. Streams by default." + }, + "CoreMemoryBlockSchema": { + "properties": { + "created_at": { + "type": "string", + "title": "Created At" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "is_template": { + "type": "boolean", + "title": "Is Template" + }, + "label": { + "type": "string", + "title": "Label" + }, + "limit": { + "type": "integer", + "title": "Limit" + }, + "metadata_": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" + }, + "template_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Name" + }, + "updated_at": { + "type": "string", + "title": "Updated At" + }, + "value": { + "type": "string", + "title": "Value" + } + }, + "type": "object", + "required": [ + "created_at", + "description", + "is_template", + "label", + "limit", + "template_name", + "updated_at", + "value" + ], + "title": "CoreMemoryBlockSchema" + }, + "CreateAgentRequest": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the agent." + }, + "memory_blocks": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/CreateBlock" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Memory Blocks", + "description": "The blocks to create in the agent's in-context memory." + }, + "tools": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tools", + "description": "The tools used by the agent." + }, + "tool_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 41, + "minLength": 41, + "pattern": "^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the tool in the format 'tool-'", + "examples": ["tool-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Ids", + "description": "The ids of the tools used by the agent." + }, + "source_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 43, + "minLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Source Ids", + "description": "Deprecated: Use `folder_ids` field instead. The ids of the sources used by the agent.", + "deprecated": true + }, + "folder_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 43, + "minLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Folder Ids", + "description": "The ids of the folders used by the agent." + }, + "block_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Block Ids", + "description": "The ids of the blocks used by the agent." + }, + "tool_rules": { + "anyOf": [ + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ChildToolRule" + }, + { + "$ref": "#/components/schemas/InitToolRule" + }, + { + "$ref": "#/components/schemas/TerminalToolRule" + }, + { + "$ref": "#/components/schemas/ConditionalToolRule" + }, + { + "$ref": "#/components/schemas/ContinueToolRule" + }, + { + "$ref": "#/components/schemas/RequiredBeforeExitToolRule" + }, + { + "$ref": "#/components/schemas/MaxCountPerStepToolRule" + }, + { + "$ref": "#/components/schemas/ParentToolRule" + }, + { + "$ref": "#/components/schemas/RequiresApprovalToolRule" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "conditional": "#/components/schemas/ConditionalToolRule", + "constrain_child_tools": "#/components/schemas/ChildToolRule", + "continue_loop": "#/components/schemas/ContinueToolRule", + "exit_loop": "#/components/schemas/TerminalToolRule", + "max_count_per_step": "#/components/schemas/MaxCountPerStepToolRule", + "parent_last_tool": "#/components/schemas/ParentToolRule", + "required_before_exit": "#/components/schemas/RequiredBeforeExitToolRule", + "requires_approval": "#/components/schemas/RequiresApprovalToolRule", + "run_first": "#/components/schemas/InitToolRule" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Rules", + "description": "The tool rules governing the agent." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "The tags associated with the agent." + }, + "system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "System", + "description": "The system prompt used by the agent." + }, + "agent_type": { + "$ref": "#/components/schemas/AgentType", + "description": "The type of agent." + }, + "initial_message_sequence": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MessageCreate" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Initial Message Sequence", + "description": "The initial set of messages to put in the agent's in-context memory." + }, + "include_base_tools": { + "type": "boolean", + "title": "Include Base Tools", + "description": "If true, attaches the Letta core tools (e.g. core_memory related functions).", + "default": true + }, + "include_multi_agent_tools": { + "type": "boolean", + "title": "Include Multi Agent Tools", + "description": "If true, attaches the Letta multi-agent tools (e.g. sending a message to another agent).", + "default": false + }, + "include_base_tool_rules": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Include Base Tool Rules", + "description": "If true, attaches the Letta base tool rules (e.g. deny all tools not explicitly allowed)." + }, + "include_default_source": { + "type": "boolean", + "title": "Include Default Source", + "description": "If true, automatically creates and attaches a default data source for this agent.", + "default": false, + "deprecated": true + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the agent." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "The metadata of the agent." + }, + "llm_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/LLMConfig" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use `model` field instead. The LLM configuration used by the agent.", + "deprecated": true + }, + "embedding_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use `embedding` field instead. The embedding configuration used by the agent.", + "deprecated": true + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "The model handle for the agent to use (format: provider/model-name)." + }, + "embedding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Embedding", + "description": "The embedding model handle used by the agent (format: provider/model-name)." + }, + "model_settings": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/OpenAIModelSettings" + }, + { + "$ref": "#/components/schemas/SGLangModelSettings" + }, + { + "$ref": "#/components/schemas/AnthropicModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleAIModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleVertexModelSettings" + }, + { + "$ref": "#/components/schemas/AzureModelSettings" + }, + { + "$ref": "#/components/schemas/XAIModelSettings" + }, + { + "$ref": "#/components/schemas/ZAIModelSettings" + }, + { + "$ref": "#/components/schemas/GroqModelSettings" + }, + { + "$ref": "#/components/schemas/DeepseekModelSettings" + }, + { + "$ref": "#/components/schemas/TogetherModelSettings" + }, + { + "$ref": "#/components/schemas/BedrockModelSettings" + }, + { + "$ref": "#/components/schemas/BasetenModelSettings" + }, + { + "$ref": "#/components/schemas/OpenRouterModelSettings" + }, + { + "$ref": "#/components/schemas/ChatGPTOAuthModelSettings" + } + ], + "discriminator": { + "propertyName": "provider_type", + "mapping": { + "anthropic": "#/components/schemas/AnthropicModelSettings", + "azure": "#/components/schemas/AzureModelSettings", + "baseten": "#/components/schemas/BasetenModelSettings", + "bedrock": "#/components/schemas/BedrockModelSettings", + "chatgpt_oauth": "#/components/schemas/ChatGPTOAuthModelSettings", + "deepseek": "#/components/schemas/DeepseekModelSettings", + "google_ai": "#/components/schemas/GoogleAIModelSettings", + "google_vertex": "#/components/schemas/GoogleVertexModelSettings", + "groq": "#/components/schemas/GroqModelSettings", + "openai": "#/components/schemas/OpenAIModelSettings", + "openrouter": "#/components/schemas/OpenRouterModelSettings", + "sglang": "#/components/schemas/SGLangModelSettings", + "together": "#/components/schemas/TogetherModelSettings", + "xai": "#/components/schemas/XAIModelSettings", + "zai": "#/components/schemas/ZAIModelSettings" + } + } + }, + { + "type": "null" + } + ], + "title": "Model Settings", + "description": "The model settings for the agent." + }, + "compaction_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/CompactionSettings-Input" + }, + { + "type": "null" + } + ], + "description": "The compaction settings configuration used for compaction." + }, + "context_window_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Context Window Limit", + "description": "The context window limit used by the agent." + }, + "embedding_chunk_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Embedding Chunk Size", + "description": "Deprecated: No longer used. The embedding chunk size used by the agent.", + "default": 300, + "deprecated": true + }, + "max_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Tokens", + "description": "Deprecated: Use `model` field to configure max output tokens instead. The maximum number of tokens to generate, including reasoning step.", + "deprecated": true + }, + "max_reasoning_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Reasoning Tokens", + "description": "Deprecated: Use `model` field to configure reasoning tokens instead. The maximum number of tokens to generate for reasoning step.", + "deprecated": true + }, + "enable_reasoner": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Reasoner", + "description": "Deprecated: Use `model` field to configure reasoning instead. Whether to enable internal extended thinking step for a reasoner model.", + "default": true, + "deprecated": true + }, + "reasoning": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Reasoning", + "description": "Deprecated: Use `model` field to configure reasoning instead. Whether to enable reasoning for this agent.", + "deprecated": true + }, + "from_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "From Template", + "description": "Deprecated: please use the 'create agents from a template' endpoint instead.", + "deprecated": true + }, + "template": { + "type": "boolean", + "title": "Template", + "description": "Deprecated: No longer used.", + "default": false, + "deprecated": true + }, + "project": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project", + "description": "Deprecated: Project should now be passed via the X-Project header instead of in the request body. If using the SDK, this can be done via the x_project parameter.", + "deprecated": true + }, + "tool_exec_environment_variables": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Tool Exec Environment Variables", + "description": "Deprecated: Use `secrets` field instead. Environment variables for tool execution.", + "deprecated": true + }, + "secrets": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Secrets", + "description": "The environment variables for tool execution specific to this agent." + }, + "memory_variables": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Memory Variables", + "description": "Deprecated: Only relevant for creating agents from a template. Use the 'create agents from a template' endpoint instead.", + "deprecated": true + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "Deprecated: No longer used. The id of the project the agent belongs to.", + "deprecated": true + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "Deprecated: No longer used. The id of the template the agent belongs to.", + "deprecated": true + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "Deprecated: No longer used. The base template id of the agent.", + "deprecated": true + }, + "identity_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 45, + "minLength": 45, + "pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Identity Ids", + "description": "The ids of the identities associated with this agent." + }, + "message_buffer_autoclear": { + "type": "boolean", + "title": "Message Buffer Autoclear", + "description": "If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case.", + "default": false + }, + "enable_sleeptime": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Sleeptime", + "description": "If set to True, memory management will move to a background agent thread." + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "Deprecated: Use `model_settings` field to configure response format instead. The response format for the agent.", + "deprecated": true + }, + "timezone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The timezone of the agent (IANA format)." + }, + "max_files_open": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Files Open", + "description": "Maximum number of files that can be open at once for this agent. Setting this too high may exceed the context window, which will break the agent." + }, + "per_file_view_window_char_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Per File View Window Char Limit", + "description": "The per-file view window character limit for this agent. Setting this too high may exceed the context window, which will break the agent." + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "Deprecated: No longer used. If set to True, the agent will be hidden.", + "deprecated": true + }, + "parallel_tool_calls": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Parallel Tool Calls", + "description": "Deprecated: Use `model_settings` to configure parallel tool calls instead. If set to True, enables parallel tool calling.", + "deprecated": true + } + }, + "type": "object", + "title": "CreateAgentRequest", + "description": "CreateAgent model specifically for POST request body, excluding user_id which comes from headers" + }, + "CreateArchivalMemory": { + "properties": { + "text": { + "type": "string", + "title": "Text", + "description": "Text to write to archival memory." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "Optional list of tags to attach to the memory." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "Optional timestamp for the memory (defaults to current UTC time)." + } + }, + "type": "object", + "required": ["text"], + "title": "CreateArchivalMemory" + }, + "CreateBatch": { + "properties": { + "requests": { + "items": { + "$ref": "#/components/schemas/LettaBatchRequest" + }, + "type": "array", + "title": "Requests", + "description": "List of requests to be processed in batch." + }, + "callback_url": { + "anyOf": [ + { + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Callback Url", + "description": "Optional URL to call via POST when the batch completes. The callback payload will be a JSON object with the following fields: {'job_id': string, 'status': string, 'completed_at': string}. Where 'job_id' is the unique batch job identifier, 'status' is the final batch status (e.g., 'completed', 'failed'), and 'completed_at' is an ISO 8601 timestamp indicating when the batch job completed." + } + }, + "type": "object", + "required": ["requests"], + "title": "CreateBatch" + }, + "CreateBlock": { + "properties": { + "value": { + "type": "string", + "title": "Value", + "description": "Value of the block." + }, + "limit": { + "type": "integer", + "title": "Limit", + "description": "Character limit of the block.", + "default": 100000 + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "template_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Name", + "description": "Name of the block if it is a template." + }, + "is_template": { + "type": "boolean", + "title": "Is Template", + "default": false + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template." + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "The base template id of the block." + }, + "deployment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deployment Id", + "description": "The id of the deployment." + }, + "entity_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Entity Id", + "description": "The id of the entity within the template." + }, + "preserve_on_migration": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Preserve On Migration", + "description": "Preserve the block on template migration.", + "default": false + }, + "label": { + "type": "string", + "title": "Label", + "description": "Label of the block." + }, + "read_only": { + "type": "boolean", + "title": "Read Only", + "description": "Whether the agent has read-only access to the block.", + "default": false + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of the block." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata of the block.", + "default": {} + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "If set to True, the block will be hidden." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "The tags to associate with the block." + } + }, + "type": "object", + "required": ["value", "label"], + "title": "CreateBlock", + "description": "Create a block" + }, + "CreateConversation": { + "properties": { + "summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Summary", + "description": "A summary of the conversation." + }, + "isolated_block_labels": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Isolated Block Labels", + "description": "List of block labels that should be isolated (conversation-specific) rather than shared across conversations. New blocks will be created as copies of the agent's blocks with these labels." + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "The model handle for this conversation (overrides agent's model). Format: provider/model-name." + }, + "model_settings": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/OpenAIModelSettings" + }, + { + "$ref": "#/components/schemas/SGLangModelSettings" + }, + { + "$ref": "#/components/schemas/AnthropicModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleAIModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleVertexModelSettings" + }, + { + "$ref": "#/components/schemas/AzureModelSettings" + }, + { + "$ref": "#/components/schemas/XAIModelSettings" + }, + { + "$ref": "#/components/schemas/ZAIModelSettings" + }, + { + "$ref": "#/components/schemas/GroqModelSettings" + }, + { + "$ref": "#/components/schemas/DeepseekModelSettings" + }, + { + "$ref": "#/components/schemas/TogetherModelSettings" + }, + { + "$ref": "#/components/schemas/BedrockModelSettings" + }, + { + "$ref": "#/components/schemas/BasetenModelSettings" + }, + { + "$ref": "#/components/schemas/OpenRouterModelSettings" + }, + { + "$ref": "#/components/schemas/ChatGPTOAuthModelSettings" + } + ], + "discriminator": { + "propertyName": "provider_type", + "mapping": { + "anthropic": "#/components/schemas/AnthropicModelSettings", + "azure": "#/components/schemas/AzureModelSettings", + "baseten": "#/components/schemas/BasetenModelSettings", + "bedrock": "#/components/schemas/BedrockModelSettings", + "chatgpt_oauth": "#/components/schemas/ChatGPTOAuthModelSettings", + "deepseek": "#/components/schemas/DeepseekModelSettings", + "google_ai": "#/components/schemas/GoogleAIModelSettings", + "google_vertex": "#/components/schemas/GoogleVertexModelSettings", + "groq": "#/components/schemas/GroqModelSettings", + "openai": "#/components/schemas/OpenAIModelSettings", + "openrouter": "#/components/schemas/OpenRouterModelSettings", + "sglang": "#/components/schemas/SGLangModelSettings", + "together": "#/components/schemas/TogetherModelSettings", + "xai": "#/components/schemas/XAIModelSettings", + "zai": "#/components/schemas/ZAIModelSettings" + } + } + }, + { + "type": "null" + } + ], + "title": "Model Settings", + "description": "The model settings for this conversation (overrides agent's model settings)." + } + }, + "type": "object", + "title": "CreateConversation", + "description": "Request model for creating a new conversation." + }, + "CreateMCPServerRequest": { + "properties": { + "server_name": { + "type": "string", + "title": "Server Name", + "description": "The name of the MCP server" + }, + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateStdioMCPServer" + }, + { + "$ref": "#/components/schemas/CreateSSEMCPServer" + }, + { + "$ref": "#/components/schemas/CreateStreamableHTTPMCPServer" + } + ], + "title": "Config", + "description": "The MCP server configuration (Stdio, SSE, or Streamable HTTP)", + "discriminator": { + "propertyName": "mcp_server_type", + "mapping": { + "sse": "#/components/schemas/CreateSSEMCPServer", + "stdio": "#/components/schemas/CreateStdioMCPServer", + "streamable_http": "#/components/schemas/CreateStreamableHTTPMCPServer" + } + } + } + }, + "additionalProperties": false, + "type": "object", + "required": ["server_name", "config"], + "title": "CreateMCPServerRequest", + "description": "Request to create a new MCP server with configuration." + }, + "CreateSSEMCPServer": { + "properties": { + "mcp_server_type": { + "type": "string", + "const": "sse", + "title": "Mcp Server Type", + "default": "sse" + }, + "server_url": { + "type": "string", + "title": "Server Url", + "description": "The URL of the server" + }, + "auth_header": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Header", + "description": "The name of the authentication header (e.g., 'Authorization')" + }, + "auth_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Token", + "description": "The authentication token or API key value" + }, + "custom_headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Headers", + "description": "Custom HTTP headers to include with requests" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["server_url"], + "title": "CreateSSEMCPServer", + "description": "Create a new SSE MCP server" + }, + "CreateStdioMCPServer": { + "properties": { + "mcp_server_type": { + "type": "string", + "const": "stdio", + "title": "Mcp Server Type", + "default": "stdio" + }, + "command": { + "type": "string", + "title": "Command", + "description": "The command to run (MCP 'local' client will run this command)" + }, + "args": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Args", + "description": "The arguments to pass to the command" + }, + "env": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Env", + "description": "Environment variables to set" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["command", "args"], + "title": "CreateStdioMCPServer", + "description": "Create a new Stdio MCP server" + }, + "CreateStreamableHTTPMCPServer": { + "properties": { + "mcp_server_type": { + "type": "string", + "const": "streamable_http", + "title": "Mcp Server Type", + "default": "streamable_http" + }, + "server_url": { + "type": "string", + "title": "Server Url", + "description": "The URL of the server" + }, + "auth_header": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Header", + "description": "The name of the authentication header (e.g., 'Authorization')" + }, + "auth_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Token", + "description": "The authentication token or API key value" + }, + "custom_headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Headers", + "description": "Custom HTTP headers to include with requests" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["server_url"], + "title": "CreateStreamableHTTPMCPServer", + "description": "Create a new Streamable HTTP MCP server" + }, + "Custom-Input": { + "properties": { + "input": { + "type": "string", + "title": "Input" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": ["input", "name"], + "title": "Custom", + "description": "The custom tool that the model called." + }, + "Custom-Output": { + "properties": { + "input": { + "type": "string", + "title": "Input" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["input", "name"], + "title": "Custom", + "description": "The custom tool that the model called." + }, + "DeepseekModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "deepseek", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "deepseek" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model." + } + }, + "type": "object", + "title": "DeepseekModelSettings", + "description": "Deepseek model configuration (OpenAI-compatible)." + }, + "DeleteDeploymentResponse": { + "properties": { + "deleted_blocks": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Deleted Blocks", + "default": [] + }, + "deleted_agents": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Deleted Agents", + "default": [] + }, + "deleted_groups": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Deleted Groups", + "default": [] + }, + "message": { + "type": "string", + "title": "Message" + } + }, + "type": "object", + "required": ["message"], + "title": "DeleteDeploymentResponse", + "description": "Response model for delete deployment operation." + }, + "DeploymentEntity": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "type": { + "type": "string", + "title": "Type" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "entity_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Entity Id" + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id" + } + }, + "type": "object", + "required": ["id", "type"], + "title": "DeploymentEntity", + "description": "A deployment entity." + }, + "DuplicateFileHandling": { + "type": "string", + "enum": ["skip", "error", "suffix", "replace"], + "title": "DuplicateFileHandling", + "description": "How to handle duplicate filenames when uploading files" + }, + "DynamicManager": { + "properties": { + "manager_type": { + "type": "string", + "const": "dynamic", + "title": "Manager Type", + "description": "", + "default": "dynamic" + }, + "manager_agent_id": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "title": "Manager Agent Id", + "description": "", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + "termination_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Termination Token", + "description": "", + "default": "DONE!" + }, + "max_turns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Turns", + "description": "" + } + }, + "type": "object", + "required": ["manager_agent_id"], + "title": "DynamicManager" + }, + "DynamicManagerSchema": { + "properties": { + "manager_type": { + "type": "string", + "const": "dynamic", + "title": "Manager Type", + "description": "", + "default": "dynamic" + }, + "manager_agent_id": { + "type": "string", + "title": "Manager Agent Id", + "description": "" + }, + "termination_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Termination Token", + "description": "", + "default": "DONE!" + }, + "max_turns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Turns", + "description": "" + } + }, + "type": "object", + "required": ["manager_agent_id"], + "title": "DynamicManagerSchema" + }, + "DynamicManagerUpdate": { + "properties": { + "manager_type": { + "type": "string", + "const": "dynamic", + "title": "Manager Type", + "description": "", + "default": "dynamic" + }, + "manager_agent_id": { + "anyOf": [ + { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + { + "type": "null" + } + ], + "title": "Manager Agent Id", + "description": "" + }, + "termination_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Termination Token", + "description": "" + }, + "max_turns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Turns", + "description": "" + } + }, + "type": "object", + "title": "DynamicManagerUpdate" + }, + "E2BSandboxConfig": { + "properties": { + "timeout": { + "type": "integer", + "title": "Timeout", + "description": "Time limit for the sandbox (in seconds).", + "default": 300 + }, + "template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template", + "description": "The E2B template id (docker image)." + }, + "pip_requirements": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Pip Requirements", + "description": "A list of pip packages to install on the E2B Sandbox" + } + }, + "type": "object", + "title": "E2BSandboxConfig" + }, + "EmbeddingConfig": { + "properties": { + "embedding_endpoint_type": { + "type": "string", + "enum": [ + "openai", + "anthropic", + "bedrock", + "google_ai", + "google_vertex", + "azure", + "groq", + "ollama", + "webui", + "webui-legacy", + "lmstudio", + "lmstudio-legacy", + "llamacpp", + "koboldcpp", + "vllm", + "hugging-face", + "mistral", + "together", + "pinecone" + ], + "title": "Embedding Endpoint Type", + "description": "The endpoint type for the model." + }, + "embedding_endpoint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Embedding Endpoint", + "description": "The endpoint for the model (`None` if local)." + }, + "embedding_model": { + "type": "string", + "title": "Embedding Model", + "description": "The model for the embedding." + }, + "embedding_dim": { + "type": "integer", + "title": "Embedding Dim", + "description": "The dimension of the embedding." + }, + "embedding_chunk_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Embedding Chunk Size", + "description": "The chunk size of the embedding.", + "default": 300 + }, + "handle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Handle", + "description": "The handle for this config, in the format provider/model-name." + }, + "batch_size": { + "type": "integer", + "title": "Batch Size", + "description": "The maximum batch size for processing embeddings.", + "default": 32 + }, + "azure_endpoint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Azure Endpoint", + "description": "The Azure endpoint for the model." + }, + "azure_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Azure Version", + "description": "The Azure version for the model." + }, + "azure_deployment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Azure Deployment", + "description": "The Azure deployment for the model." + } + }, + "type": "object", + "required": [ + "embedding_endpoint_type", + "embedding_model", + "embedding_dim" + ], + "title": "EmbeddingConfig", + "description": "Configuration for embedding model connection and processing parameters." + }, + "EmbeddingModel": { + "properties": { + "handle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Handle", + "description": "The handle for this config, in the format provider/model-name." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The actual model name used by the provider" + }, + "display_name": { + "type": "string", + "title": "Display Name", + "description": "Display name for the model shown in UI" + }, + "provider_type": { + "$ref": "#/components/schemas/ProviderType", + "description": "The type of the provider" + }, + "provider_name": { + "type": "string", + "title": "Provider Name", + "description": "The name of the provider" + }, + "model_type": { + "type": "string", + "const": "embedding", + "title": "Model Type", + "description": "Type of model (llm or embedding)", + "default": "embedding" + }, + "embedding_endpoint_type": { + "type": "string", + "enum": [ + "openai", + "anthropic", + "bedrock", + "google_ai", + "google_vertex", + "azure", + "groq", + "ollama", + "webui", + "webui-legacy", + "lmstudio", + "lmstudio-legacy", + "llamacpp", + "koboldcpp", + "vllm", + "hugging-face", + "mistral", + "together", + "pinecone" + ], + "title": "Embedding Endpoint Type", + "description": "Deprecated: Use 'provider_type' field instead. The endpoint type for the embedding model.", + "deprecated": true + }, + "embedding_endpoint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Embedding Endpoint", + "description": "Deprecated: The endpoint for the model.", + "deprecated": true + }, + "embedding_model": { + "type": "string", + "title": "Embedding Model", + "description": "Deprecated: Use 'name' field instead. Embedding model name.", + "deprecated": true + }, + "embedding_dim": { + "type": "integer", + "title": "Embedding Dim", + "description": "The dimension of the embedding" + }, + "embedding_chunk_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Embedding Chunk Size", + "description": "Deprecated: The chunk size of the embedding.", + "default": 300, + "deprecated": true + }, + "batch_size": { + "type": "integer", + "title": "Batch Size", + "description": "Deprecated: The maximum batch size for processing embeddings.", + "default": 32, + "deprecated": true + }, + "azure_endpoint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Azure Endpoint", + "description": "Deprecated: The Azure endpoint for the model.", + "deprecated": true + }, + "azure_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Azure Version", + "description": "Deprecated: The Azure version for the model.", + "deprecated": true + }, + "azure_deployment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Azure Deployment", + "description": "Deprecated: The Azure deployment for the model.", + "deprecated": true + } + }, + "type": "object", + "required": [ + "name", + "display_name", + "provider_type", + "provider_name", + "embedding_endpoint_type", + "embedding_model", + "embedding_dim" + ], + "title": "EmbeddingModel" + }, + "EventMessage": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "message_type": { + "type": "string", + "const": "event_message", + "title": "Message Type", + "default": "event_message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err" + }, + "seq_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seq Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + }, + "event_type": { + "type": "string", + "const": "compaction", + "title": "Event Type" + }, + "event_data": { + "additionalProperties": true, + "type": "object", + "title": "Event Data" + } + }, + "type": "object", + "required": ["id", "date", "event_type", "event_data"], + "title": "EventMessage", + "description": "A message for notifying the developer that an event that has occured (e.g. a compaction). Events are NOT part of the context window." + }, + "ExportAgentRequest": { + "properties": { + "skills": { + "items": { + "$ref": "#/components/schemas/SkillSchema" + }, + "type": "array", + "title": "Skills", + "description": "Skills to include in the export. Each skill must have a name and files (including SKILL.md)." + }, + "conversation_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Conversation Id", + "description": "Conversation ID to export. If provided, uses messages from this conversation instead of the agent's global message history." + }, + "scrub_messages": { + "type": "boolean", + "title": "Scrub Messages", + "description": "If True, excludes all messages from the export. Useful for sharing agent configs without conversation history.", + "default": false + } + }, + "type": "object", + "title": "ExportAgentRequest", + "description": "Request body for POST /export endpoint." + }, + "FeedbackType": { + "type": "string", + "enum": ["positive", "negative"], + "title": "FeedbackType" + }, + "File": { + "properties": { + "file": { + "$ref": "#/components/schemas/FileFile" + }, + "type": { + "type": "string", + "const": "file", + "title": "Type" + } + }, + "type": "object", + "required": ["file", "type"], + "title": "File", + "description": "Learn about [file inputs](https://platform.openai.com/docs/guides/text) for text generation." + }, + "FileAgentSchema": { + "properties": { + "agent_id": { + "type": "string", + "title": "Agent Id", + "description": "Unique identifier of the agent." + }, + "file_id": { + "type": "string", + "title": "File Id", + "description": "Unique identifier of the file." + }, + "source_id": { + "type": "string", + "title": "Source Id", + "description": "Deprecated: Use `folder_id` field instead. Unique identifier of the source.", + "deprecated": true + }, + "file_name": { + "type": "string", + "title": "File Name", + "description": "Name of the file." + }, + "is_open": { + "type": "boolean", + "title": "Is Open", + "description": "True if the agent currently has the file open.", + "default": true + }, + "visible_content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Visible Content", + "description": "Portion of the file the agent is focused on (may be large)." + }, + "last_accessed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Accessed At", + "description": "UTC timestamp of the agent's most recent access to this file." + }, + "start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Start Line", + "description": "Starting line number (1-indexed) when file was opened with line range." + }, + "end_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "End Line", + "description": "Ending line number (exclusive) when file was opened with line range." + }, + "id": { + "type": "string", + "title": "Id", + "description": "Human-readable identifier for this file-agent relationship in the file" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["agent_id", "file_id", "source_id", "file_name", "id"], + "title": "FileAgentSchema", + "description": "File-Agent relationship with human-readable ID for agent file" + }, + "FileBlock": { + "properties": { + "value": { + "type": "string", + "title": "Value", + "description": "Value of the block." + }, + "limit": { + "type": "integer", + "title": "Limit", + "description": "Character limit of the block.", + "default": 100000 + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "template_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Name", + "description": "Name of the block if it is a template." + }, + "is_template": { + "type": "boolean", + "title": "Is Template", + "description": "Whether the block is a template (e.g. saved human/persona options).", + "default": false + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template." + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "The base template id of the block." + }, + "deployment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deployment Id", + "description": "The id of the deployment." + }, + "entity_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Entity Id", + "description": "The id of the entity within the template." + }, + "preserve_on_migration": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Preserve On Migration", + "description": "Preserve the block on template migration.", + "default": false + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Label", + "description": "Label of the block (e.g. 'human', 'persona') in the context window." + }, + "read_only": { + "type": "boolean", + "title": "Read Only", + "description": "Whether the agent has read-only access to the block.", + "default": false + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of the block." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata of the block.", + "default": {} + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "If set to True, the block will be hidden." + }, + "id": { + "type": "string", + "pattern": "^block-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Block", + "examples": ["block-123e4567-e89b-12d3-a456-426614174000"] + }, + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this Block." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that last updated this Block." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "The tags associated with the block.", + "default": [] + }, + "file_id": { + "type": "string", + "title": "File Id", + "description": "Unique identifier of the file." + }, + "source_id": { + "type": "string", + "title": "Source Id", + "description": "Deprecated: Use `folder_id` field instead. Unique identifier of the source.", + "deprecated": true + }, + "is_open": { + "type": "boolean", + "title": "Is Open", + "description": "True if the agent currently has the file open." + }, + "last_accessed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Accessed At", + "description": "UTC timestamp of the agent’s most recent access to this file. Any operations from the open, close, or search tools will update this field." + } + }, + "type": "object", + "required": ["value", "file_id", "source_id", "is_open"], + "title": "FileBlock" + }, + "FileFile": { + "properties": { + "file_data": { + "type": "string", + "title": "File Data" + }, + "file_id": { + "type": "string", + "title": "File Id" + }, + "filename": { + "type": "string", + "title": "Filename" + } + }, + "type": "object", + "title": "FileFile" + }, + "FileMetadata": { + "properties": { + "source_id": { + "type": "string", + "title": "Source Id", + "description": "Deprecated: Use `folder_id` field instead. The unique identifier of the source associated with the document.", + "deprecated": true + }, + "file_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Name", + "description": "The name of the file." + }, + "original_file_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Original File Name", + "description": "The original name of the file as uploaded." + }, + "file_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Path", + "description": "The path to the file." + }, + "file_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Type", + "description": "The type of the file (MIME type)." + }, + "file_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "File Size", + "description": "The size of the file in bytes." + }, + "file_creation_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Creation Date", + "description": "The creation date of the file." + }, + "file_last_modified_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Last Modified Date", + "description": "The last modified date of the file." + }, + "processing_status": { + "$ref": "#/components/schemas/FileProcessingStatus", + "description": "The current processing status of the file (e.g. pending, parsing, embedding, completed, error).", + "default": "pending" + }, + "error_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Message", + "description": "Optional error message if the file failed processing." + }, + "total_chunks": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Total Chunks", + "description": "Total number of chunks for the file." + }, + "chunks_embedded": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Chunks Embedded", + "description": "Number of chunks that have been embedded." + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Content", + "description": "Optional full-text content of the file; only populated on demand due to its size." + }, + "id": { + "type": "string", + "pattern": "^file-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the File", + "examples": ["file-123e4567-e89b-12d3-a456-426614174000"] + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "The creation date of the file." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The update date of the file." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["source_id"], + "title": "FileMetadata", + "description": "Representation of a single FileMetadata" + }, + "FileProcessingStatus": { + "type": "string", + "enum": ["pending", "parsing", "embedding", "completed", "error"], + "title": "FileProcessingStatus" + }, + "FileSchema": { + "properties": { + "source_id": { + "type": "string", + "title": "Source Id", + "description": "Deprecated: Use `folder_id` field instead. The unique identifier of the source associated with the document.", + "deprecated": true + }, + "file_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Name", + "description": "The name of the file." + }, + "original_file_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Original File Name", + "description": "The original name of the file as uploaded." + }, + "file_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Path", + "description": "The path to the file." + }, + "file_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Type", + "description": "The type of the file (MIME type)." + }, + "file_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "File Size", + "description": "The size of the file in bytes." + }, + "file_creation_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Creation Date", + "description": "The creation date of the file." + }, + "file_last_modified_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Last Modified Date", + "description": "The last modified date of the file." + }, + "processing_status": { + "$ref": "#/components/schemas/FileProcessingStatus", + "description": "The current processing status of the file (e.g. pending, parsing, embedding, completed, error).", + "default": "pending" + }, + "error_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Message", + "description": "Optional error message if the file failed processing." + }, + "total_chunks": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Total Chunks", + "description": "Total number of chunks for the file." + }, + "chunks_embedded": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Chunks Embedded", + "description": "Number of chunks that have been embedded." + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Content", + "description": "Optional full-text content of the file; only populated on demand due to its size." + }, + "id": { + "type": "string", + "title": "Id", + "description": "Human-readable identifier for this file in the file" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["source_id", "id"], + "title": "FileSchema", + "description": "File with human-readable ID for agent file" + }, + "FileStats": { + "properties": { + "file_id": { + "type": "string", + "title": "File Id", + "description": "Unique identifier of the file" + }, + "file_name": { + "type": "string", + "title": "File Name", + "description": "Name of the file" + }, + "file_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "File Size", + "description": "Size of the file in bytes" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["file_id", "file_name"], + "title": "FileStats", + "description": "File statistics for metadata endpoint" + }, + "Folder": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the folder." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the folder." + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Instructions", + "description": "Instructions for how to use the folder." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata associated with the folder." + }, + "id": { + "type": "string", + "pattern": "^source-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Source", + "examples": ["source-123e4567-e89b-12d3-a456-426614174000"] + }, + "embedding_config": { + "$ref": "#/components/schemas/EmbeddingConfig", + "description": "The embedding configuration used by the folder." + }, + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this Tool." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this Tool." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "The timestamp when the folder was created." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the folder was last updated." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["name", "embedding_config"], + "title": "Folder", + "description": "Representation of a folder, which is a collection of files and passages." + }, + "Function-Output": { + "properties": { + "arguments": { + "type": "string", + "title": "Arguments" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["arguments", "name"], + "title": "Function", + "description": "The function that the model called." + }, + "FunctionCall-Input": { + "properties": { + "arguments": { + "type": "string", + "title": "Arguments" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": ["arguments", "name"], + "title": "FunctionCall", + "description": "Deprecated and replaced by `tool_calls`.\n\nThe name and arguments of a function that should be called, as generated by the model." + }, + "FunctionCall-Output": { + "properties": { + "arguments": { + "type": "string", + "title": "Arguments" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["arguments", "name"], + "title": "FunctionCall", + "description": "Deprecated and replaced by `tool_calls`.\n\nThe name and arguments of a function that should be called, as generated by the model." + }, + "FunctionDefinition": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "parameters": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Parameters" + }, + "strict": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Strict" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["name"], + "title": "FunctionDefinition" + }, + "FunctionTool": { + "properties": { + "function": { + "$ref": "#/components/schemas/FunctionDefinition" + }, + "type": { + "type": "string", + "const": "function", + "title": "Type" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["function", "type"], + "title": "FunctionTool" + }, + "GeminiThinkingConfig": { + "properties": { + "include_thoughts": { + "type": "boolean", + "title": "Include Thoughts", + "description": "Whether to include thoughts in the model's response.", + "default": true + }, + "thinking_budget": { + "type": "integer", + "title": "Thinking Budget", + "description": "The thinking budget for the model.", + "default": 1024 + } + }, + "type": "object", + "title": "GeminiThinkingConfig" + }, + "GenerateRequest": { + "properties": { + "prompt": { + "type": "string", + "minLength": 1, + "title": "Prompt", + "description": "The prompt/message to send to the LLM" + }, + "system_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "System Prompt", + "description": "Optional system prompt to prepend to the conversation" + }, + "override_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override Model", + "description": "Model handle to use instead of agent's default (e.g., 'openai/gpt-4', 'anthropic/claude-3-5-sonnet')" + }, + "response_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Response Schema", + "description": "JSON schema for structured output. When provided, the LLM will be forced to return a response matching this schema via tool calling. The schema should follow JSON Schema format with 'properties' and optionally 'required' fields." + } + }, + "type": "object", + "required": ["prompt"], + "title": "GenerateRequest", + "description": "Request for direct LLM generation without agent processing." + }, + "GenerateResponse": { + "properties": { + "content": { + "type": "string", + "title": "Content", + "description": "The LLM's response text" + }, + "model": { + "type": "string", + "title": "Model", + "description": "The model that generated this response" + }, + "usage": { + "$ref": "#/components/schemas/LettaUsageStatistics", + "description": "Token usage statistics" + } + }, + "type": "object", + "required": ["content", "model", "usage"], + "title": "GenerateResponse", + "description": "Response from direct LLM generation." + }, + "GenerateToolInput": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name", + "description": "Name of the tool to generate code for" + }, + "prompt": { + "type": "string", + "title": "Prompt", + "description": "User prompt to generate code" + }, + "handle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Handle", + "description": "Handle of the tool to generate code for" + }, + "starter_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Starter Code", + "description": "Python source code to parse for JSON schema" + }, + "validation_errors": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Validation Errors", + "description": "List of validation errors" + } + }, + "type": "object", + "required": ["tool_name", "prompt", "validation_errors"], + "title": "GenerateToolInput" + }, + "GenerateToolOutput": { + "properties": { + "tool": { + "$ref": "#/components/schemas/Tool", + "description": "Generated tool" + }, + "sample_args": { + "additionalProperties": true, + "type": "object", + "title": "Sample Args", + "description": "Sample arguments for the tool" + }, + "response": { + "type": "string", + "title": "Response", + "description": "Response from the assistant" + } + }, + "type": "object", + "required": ["tool", "sample_args", "response"], + "title": "GenerateToolOutput" + }, + "GoogleAIModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 65536 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "google_ai", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "google_ai" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "thinking_config": { + "$ref": "#/components/schemas/GeminiThinkingConfig", + "description": "The thinking configuration for the model.", + "default": { + "include_thoughts": true, + "thinking_budget": 1024 + } + }, + "response_schema": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Schema", + "description": "The response schema for the model." + } + }, + "type": "object", + "title": "GoogleAIModelSettings" + }, + "GoogleVertexModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 65536 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "google_vertex", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "google_vertex" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "thinking_config": { + "$ref": "#/components/schemas/GeminiThinkingConfig", + "description": "The thinking configuration for the model.", + "default": { + "include_thoughts": true, + "thinking_budget": 1024 + } + }, + "response_schema": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Schema", + "description": "The response schema for the model." + } + }, + "type": "object", + "title": "GoogleVertexModelSettings" + }, + "GroqModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "groq", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "groq" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model." + } + }, + "type": "object", + "title": "GroqModelSettings", + "description": "Groq model configuration (OpenAI-compatible)." + }, + "Group": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of the group. Assigned by the database." + }, + "manager_type": { + "$ref": "#/components/schemas/ManagerType", + "description": "" + }, + "agent_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Agent Ids", + "description": "" + }, + "description": { + "type": "string", + "title": "Description", + "description": "" + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template." + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "The base template id." + }, + "deployment_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Deployment Id", + "description": "The id of the deployment." + }, + "shared_block_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Shared Block Ids", + "description": "", + "default": [], + "deprecated": true + }, + "manager_agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Manager Agent Id", + "description": "" + }, + "termination_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Termination Token", + "description": "" + }, + "max_turns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Turns", + "description": "" + }, + "sleeptime_agent_frequency": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Sleeptime Agent Frequency", + "description": "" + }, + "turns_counter": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Turns Counter", + "description": "" + }, + "last_processed_message_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Processed Message Id", + "description": "" + }, + "max_message_buffer_length": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Message Buffer Length", + "description": "The desired maximum length of messages in the context window of the convo agent. This is a best effort, and may be off slightly due to user/assistant interleaving." + }, + "min_message_buffer_length": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Min Message Buffer Length", + "description": "The desired minimum length of messages in the context window of the convo agent. This is a best effort, and may be off-by-one due to user/assistant interleaving." + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "If set to True, the group will be hidden." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["id", "manager_type", "agent_ids", "description"], + "title": "Group" + }, + "GroupCreate": { + "properties": { + "agent_ids": { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array", + "title": "Agent Ids", + "description": "" + }, + "description": { + "type": "string", + "title": "Description", + "description": "" + }, + "manager_config": { + "oneOf": [ + { + "$ref": "#/components/schemas/RoundRobinManager" + }, + { + "$ref": "#/components/schemas/SupervisorManager" + }, + { + "$ref": "#/components/schemas/DynamicManager" + }, + { + "$ref": "#/components/schemas/SleeptimeManager" + }, + { + "$ref": "#/components/schemas/VoiceSleeptimeManager" + } + ], + "title": "Manager Config", + "description": "", + "default": { + "manager_type": "round_robin" + }, + "discriminator": { + "propertyName": "manager_type", + "mapping": { + "dynamic": "#/components/schemas/DynamicManager", + "round_robin": "#/components/schemas/RoundRobinManager", + "sleeptime": "#/components/schemas/SleeptimeManager", + "supervisor": "#/components/schemas/SupervisorManager", + "voice_sleeptime": "#/components/schemas/VoiceSleeptimeManager" + } + } + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "shared_block_ids": { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array", + "title": "Shared Block Ids", + "description": "", + "default": [], + "deprecated": true + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "If set to True, the group will be hidden." + } + }, + "type": "object", + "required": ["agent_ids", "description"], + "title": "GroupCreate" + }, + "GroupSchema": { + "properties": { + "agent_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Agent Ids", + "description": "List of agent IDs in this group" + }, + "description": { + "type": "string", + "title": "Description", + "description": "" + }, + "manager_config": { + "oneOf": [ + { + "$ref": "#/components/schemas/RoundRobinManager" + }, + { + "$ref": "#/components/schemas/SupervisorManagerSchema" + }, + { + "$ref": "#/components/schemas/DynamicManagerSchema" + }, + { + "$ref": "#/components/schemas/SleeptimeManagerSchema" + }, + { + "$ref": "#/components/schemas/VoiceSleeptimeManagerSchema" + } + ], + "title": "Manager Config", + "description": "", + "default": { + "manager_type": "round_robin" + }, + "discriminator": { + "propertyName": "manager_type", + "mapping": { + "dynamic": "#/components/schemas/DynamicManagerSchema", + "round_robin": "#/components/schemas/RoundRobinManager", + "sleeptime": "#/components/schemas/SleeptimeManagerSchema", + "supervisor": "#/components/schemas/SupervisorManagerSchema", + "voice_sleeptime": "#/components/schemas/VoiceSleeptimeManagerSchema" + } + } + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "shared_block_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Shared Block Ids", + "description": "List of shared block IDs", + "default": [] + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "If set to True, the group will be hidden." + }, + "id": { + "type": "string", + "title": "Id", + "description": "Human-readable identifier for this group in the file" + } + }, + "type": "object", + "required": ["agent_ids", "description", "id"], + "title": "GroupSchema", + "description": "Group with human-readable ID for agent file" + }, + "GroupUpdate": { + "properties": { + "agent_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Agent Ids", + "description": "" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "" + }, + "manager_config": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/RoundRobinManagerUpdate" + }, + { + "$ref": "#/components/schemas/SupervisorManagerUpdate" + }, + { + "$ref": "#/components/schemas/DynamicManagerUpdate" + }, + { + "$ref": "#/components/schemas/SleeptimeManagerUpdate" + }, + { + "$ref": "#/components/schemas/VoiceSleeptimeManagerUpdate" + } + ], + "discriminator": { + "propertyName": "manager_type", + "mapping": { + "dynamic": "#/components/schemas/DynamicManagerUpdate", + "round_robin": "#/components/schemas/RoundRobinManagerUpdate", + "sleeptime": "#/components/schemas/SleeptimeManagerUpdate", + "supervisor": "#/components/schemas/SupervisorManagerUpdate", + "voice_sleeptime": "#/components/schemas/VoiceSleeptimeManagerUpdate" + } + } + }, + { + "type": "null" + } + ], + "title": "Manager Config", + "description": "" + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "shared_block_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Shared Block Ids", + "description": "", + "deprecated": true + } + }, + "type": "object", + "title": "GroupUpdate" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "Health": { + "properties": { + "version": { + "type": "string", + "title": "Version" + }, + "status": { + "type": "string", + "title": "Status" + } + }, + "type": "object", + "required": ["version", "status"], + "title": "Health", + "description": "Health check response body" + }, + "HiddenReasoningMessage": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "message_type": { + "type": "string", + "const": "hidden_reasoning_message", + "title": "Message Type", + "description": "The type of the message.", + "default": "hidden_reasoning_message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err" + }, + "seq_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seq Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + }, + "state": { + "type": "string", + "enum": ["redacted", "omitted"], + "title": "State" + }, + "hidden_reasoning": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Hidden Reasoning" + } + }, + "type": "object", + "required": ["id", "date", "state"], + "title": "HiddenReasoningMessage", + "description": "Representation of an agent's internal reasoning where reasoning content\nhas been hidden from the response.\n\nArgs:\n id (str): The ID of the message\n date (datetime): The date the message was created in ISO format\n name (Optional[str]): The name of the sender of the message\n state (Literal[\"redacted\", \"omitted\"]): Whether the reasoning\n content was redacted by the provider or simply omitted by the API\n hidden_reasoning (Optional[str]): The internal reasoning of the agent" + }, + "Identity": { + "properties": { + "id": { + "type": "string", + "pattern": "^identity-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Identity", + "examples": ["identity-123e4567-e89b-12d3-a456-426614174000"] + }, + "identifier_key": { + "type": "string", + "title": "Identifier Key", + "description": "External, user-generated identifier key of the identity." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the identity." + }, + "identity_type": { + "$ref": "#/components/schemas/IdentityType", + "description": "The type of the identity." + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The project id of the identity, if applicable." + }, + "agent_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Agent Ids", + "description": "The IDs of the agents associated with the identity.", + "deprecated": true + }, + "block_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Block Ids", + "description": "The IDs of the blocks associated with the identity.", + "deprecated": true + }, + "properties": { + "items": { + "$ref": "#/components/schemas/IdentityProperty" + }, + "type": "array", + "title": "Properties", + "description": "List of properties associated with the identity" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "identifier_key", + "name", + "identity_type", + "agent_ids", + "block_ids" + ], + "title": "Identity" + }, + "IdentityCreate": { + "properties": { + "identifier_key": { + "type": "string", + "title": "Identifier Key", + "description": "External, user-generated identifier key of the identity." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the identity." + }, + "identity_type": { + "$ref": "#/components/schemas/IdentityType", + "description": "The type of the identity." + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The project id of the identity, if applicable." + }, + "agent_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Agent Ids", + "description": "The agent ids that are associated with the identity.", + "deprecated": true + }, + "block_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Block Ids", + "description": "The IDs of the blocks associated with the identity.", + "deprecated": true + }, + "properties": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/IdentityProperty" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Properties", + "description": "List of properties associated with the identity." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["identifier_key", "name", "identity_type"], + "title": "IdentityCreate" + }, + "IdentityProperty": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "The key of the property" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "additionalProperties": true, + "type": "object" + } + ], + "title": "Value", + "description": "The value of the property" + }, + "type": { + "$ref": "#/components/schemas/IdentityPropertyType", + "description": "The type of the property" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["key", "value", "type"], + "title": "IdentityProperty", + "description": "A property of an identity" + }, + "IdentityPropertyType": { + "type": "string", + "enum": ["string", "number", "boolean", "json"], + "title": "IdentityPropertyType", + "description": "Enum to represent the type of the identity property." + }, + "IdentityType": { + "type": "string", + "enum": ["org", "user", "other"], + "title": "IdentityType", + "description": "Enum to represent the type of the identity." + }, + "IdentityUpdate": { + "properties": { + "identifier_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Identifier Key", + "description": "External, user-generated identifier key of the identity." + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "The name of the identity." + }, + "identity_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/IdentityType" + }, + { + "type": "null" + } + ], + "description": "The type of the identity." + }, + "agent_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Agent Ids", + "description": "The agent ids that are associated with the identity.", + "deprecated": true + }, + "block_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Block Ids", + "description": "The IDs of the blocks associated with the identity.", + "deprecated": true + }, + "properties": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/IdentityProperty" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Properties", + "description": "List of properties associated with the identity." + } + }, + "additionalProperties": false, + "type": "object", + "title": "IdentityUpdate" + }, + "IdentityUpsert": { + "properties": { + "identifier_key": { + "type": "string", + "title": "Identifier Key", + "description": "External, user-generated identifier key of the identity." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the identity." + }, + "identity_type": { + "$ref": "#/components/schemas/IdentityType", + "description": "The type of the identity." + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The project id of the identity, if applicable." + }, + "agent_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Agent Ids", + "description": "The agent ids that are associated with the identity.", + "deprecated": true + }, + "block_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Block Ids", + "description": "The IDs of the blocks associated with the identity.", + "deprecated": true + }, + "properties": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/IdentityProperty" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Properties", + "description": "List of properties associated with the identity." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["identifier_key", "name", "identity_type"], + "title": "IdentityUpsert" + }, + "ImageContent": { + "properties": { + "type": { + "type": "string", + "const": "image", + "title": "Type", + "description": "The type of the message.", + "default": "image" + }, + "source": { + "oneOf": [ + { + "$ref": "#/components/schemas/UrlImage" + }, + { + "$ref": "#/components/schemas/Base64Image" + }, + { + "$ref": "#/components/schemas/LettaImage" + } + ], + "title": "Source", + "description": "The source of the image.", + "discriminator": { + "propertyName": "type", + "mapping": { + "base64": "#/components/schemas/Base64Image", + "letta": "#/components/schemas/LettaImage", + "url": "#/components/schemas/UrlImage" + } + } + } + }, + "type": "object", + "required": ["source"], + "title": "ImageContent" + }, + "ImageURL": { + "properties": { + "url": { + "type": "string", + "title": "Url" + }, + "detail": { + "type": "string", + "enum": ["auto", "low", "high"], + "title": "Detail" + } + }, + "type": "object", + "required": ["url"], + "title": "ImageURL" + }, + "ImportedAgentsResponse": { + "properties": { + "agent_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Agent Ids", + "description": "List of IDs of the imported agents" + } + }, + "type": "object", + "required": ["agent_ids"], + "title": "ImportedAgentsResponse", + "description": "Response model for imported agents" + }, + "InitToolRule": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name", + "description": "The name of the tool. Must exist in the database for the user's organization." + }, + "type": { + "type": "string", + "const": "run_first", + "title": "Type", + "default": "run_first" + }, + "prompt_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt Template", + "description": "Optional template string (ignored). Rendering uses fast built-in formatting for performance." + }, + "args": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Args", + "description": "Optional prefilled arguments for this tool. When present, these values will override any LLM-provided arguments with the same keys during invocation. Keys must match the tool's parameter names and values must satisfy the tool's JSON schema. Supports partial prefill; non-overlapping parameters are left to the model." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["tool_name"], + "title": "InitToolRule", + "description": "Represents the initial tool rule configuration." + }, + "InputAudio": { + "properties": { + "data": { + "type": "string", + "title": "Data" + }, + "format": { + "type": "string", + "enum": ["wav", "mp3"], + "title": "Format" + } + }, + "type": "object", + "required": ["data", "format"], + "title": "InputAudio" + }, + "InternalTemplateAgentCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the agent." + }, + "memory_blocks": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/CreateBlock" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Memory Blocks", + "description": "The blocks to create in the agent's in-context memory." + }, + "tools": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tools", + "description": "The tools used by the agent." + }, + "tool_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 41, + "minLength": 41, + "pattern": "^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the tool in the format 'tool-'", + "examples": ["tool-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Ids", + "description": "The ids of the tools used by the agent." + }, + "source_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 43, + "minLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Source Ids", + "description": "Deprecated: Use `folder_ids` field instead. The ids of the sources used by the agent.", + "deprecated": true + }, + "folder_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 43, + "minLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Folder Ids", + "description": "The ids of the folders used by the agent." + }, + "block_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Block Ids", + "description": "The ids of the blocks used by the agent." + }, + "tool_rules": { + "anyOf": [ + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ChildToolRule" + }, + { + "$ref": "#/components/schemas/InitToolRule" + }, + { + "$ref": "#/components/schemas/TerminalToolRule" + }, + { + "$ref": "#/components/schemas/ConditionalToolRule" + }, + { + "$ref": "#/components/schemas/ContinueToolRule" + }, + { + "$ref": "#/components/schemas/RequiredBeforeExitToolRule" + }, + { + "$ref": "#/components/schemas/MaxCountPerStepToolRule" + }, + { + "$ref": "#/components/schemas/ParentToolRule" + }, + { + "$ref": "#/components/schemas/RequiresApprovalToolRule" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "conditional": "#/components/schemas/ConditionalToolRule", + "constrain_child_tools": "#/components/schemas/ChildToolRule", + "continue_loop": "#/components/schemas/ContinueToolRule", + "exit_loop": "#/components/schemas/TerminalToolRule", + "max_count_per_step": "#/components/schemas/MaxCountPerStepToolRule", + "parent_last_tool": "#/components/schemas/ParentToolRule", + "required_before_exit": "#/components/schemas/RequiredBeforeExitToolRule", + "requires_approval": "#/components/schemas/RequiresApprovalToolRule", + "run_first": "#/components/schemas/InitToolRule" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Rules", + "description": "The tool rules governing the agent." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "The tags associated with the agent." + }, + "system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "System", + "description": "The system prompt used by the agent." + }, + "agent_type": { + "$ref": "#/components/schemas/AgentType", + "description": "The type of agent." + }, + "initial_message_sequence": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MessageCreate" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Initial Message Sequence", + "description": "The initial set of messages to put in the agent's in-context memory." + }, + "include_base_tools": { + "type": "boolean", + "title": "Include Base Tools", + "description": "If true, attaches the Letta core tools (e.g. core_memory related functions).", + "default": true + }, + "include_multi_agent_tools": { + "type": "boolean", + "title": "Include Multi Agent Tools", + "description": "If true, attaches the Letta multi-agent tools (e.g. sending a message to another agent).", + "default": false + }, + "include_base_tool_rules": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Include Base Tool Rules", + "description": "If true, attaches the Letta base tool rules (e.g. deny all tools not explicitly allowed)." + }, + "include_default_source": { + "type": "boolean", + "title": "Include Default Source", + "description": "If true, automatically creates and attaches a default data source for this agent.", + "default": false, + "deprecated": true + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the agent." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "The metadata of the agent." + }, + "llm_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/LLMConfig" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use `model` field instead. The LLM configuration used by the agent.", + "deprecated": true + }, + "embedding_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use `embedding` field instead. The embedding configuration used by the agent.", + "deprecated": true + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "The model handle for the agent to use (format: provider/model-name)." + }, + "embedding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Embedding", + "description": "The embedding model handle used by the agent (format: provider/model-name)." + }, + "model_settings": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/OpenAIModelSettings" + }, + { + "$ref": "#/components/schemas/SGLangModelSettings" + }, + { + "$ref": "#/components/schemas/AnthropicModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleAIModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleVertexModelSettings" + }, + { + "$ref": "#/components/schemas/AzureModelSettings" + }, + { + "$ref": "#/components/schemas/XAIModelSettings" + }, + { + "$ref": "#/components/schemas/ZAIModelSettings" + }, + { + "$ref": "#/components/schemas/GroqModelSettings" + }, + { + "$ref": "#/components/schemas/DeepseekModelSettings" + }, + { + "$ref": "#/components/schemas/TogetherModelSettings" + }, + { + "$ref": "#/components/schemas/BedrockModelSettings" + }, + { + "$ref": "#/components/schemas/BasetenModelSettings" + }, + { + "$ref": "#/components/schemas/OpenRouterModelSettings" + }, + { + "$ref": "#/components/schemas/ChatGPTOAuthModelSettings" + } + ], + "discriminator": { + "propertyName": "provider_type", + "mapping": { + "anthropic": "#/components/schemas/AnthropicModelSettings", + "azure": "#/components/schemas/AzureModelSettings", + "baseten": "#/components/schemas/BasetenModelSettings", + "bedrock": "#/components/schemas/BedrockModelSettings", + "chatgpt_oauth": "#/components/schemas/ChatGPTOAuthModelSettings", + "deepseek": "#/components/schemas/DeepseekModelSettings", + "google_ai": "#/components/schemas/GoogleAIModelSettings", + "google_vertex": "#/components/schemas/GoogleVertexModelSettings", + "groq": "#/components/schemas/GroqModelSettings", + "openai": "#/components/schemas/OpenAIModelSettings", + "openrouter": "#/components/schemas/OpenRouterModelSettings", + "sglang": "#/components/schemas/SGLangModelSettings", + "together": "#/components/schemas/TogetherModelSettings", + "xai": "#/components/schemas/XAIModelSettings", + "zai": "#/components/schemas/ZAIModelSettings" + } + } + }, + { + "type": "null" + } + ], + "title": "Model Settings", + "description": "The model settings for the agent." + }, + "compaction_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/CompactionSettings-Input" + }, + { + "type": "null" + } + ], + "description": "The compaction settings configuration used for compaction." + }, + "context_window_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Context Window Limit", + "description": "The context window limit used by the agent." + }, + "embedding_chunk_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Embedding Chunk Size", + "description": "Deprecated: No longer used. The embedding chunk size used by the agent.", + "default": 300, + "deprecated": true + }, + "max_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Tokens", + "description": "Deprecated: Use `model` field to configure max output tokens instead. The maximum number of tokens to generate, including reasoning step.", + "deprecated": true + }, + "max_reasoning_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Reasoning Tokens", + "description": "Deprecated: Use `model` field to configure reasoning tokens instead. The maximum number of tokens to generate for reasoning step.", + "deprecated": true + }, + "enable_reasoner": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Reasoner", + "description": "Deprecated: Use `model` field to configure reasoning instead. Whether to enable internal extended thinking step for a reasoner model.", + "default": true, + "deprecated": true + }, + "reasoning": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Reasoning", + "description": "Deprecated: Use `model` field to configure reasoning instead. Whether to enable reasoning for this agent.", + "deprecated": true + }, + "from_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "From Template", + "description": "Deprecated: please use the 'create agents from a template' endpoint instead.", + "deprecated": true + }, + "template": { + "type": "boolean", + "title": "Template", + "description": "Deprecated: No longer used.", + "default": false, + "deprecated": true + }, + "project": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project", + "description": "Deprecated: Project should now be passed via the X-Project header instead of in the request body. If using the SDK, this can be done via the x_project parameter.", + "deprecated": true + }, + "tool_exec_environment_variables": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Tool Exec Environment Variables", + "description": "Deprecated: Use `secrets` field instead. Environment variables for tool execution.", + "deprecated": true + }, + "secrets": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Secrets", + "description": "The environment variables for tool execution specific to this agent." + }, + "memory_variables": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Memory Variables", + "description": "Deprecated: Only relevant for creating agents from a template. Use the 'create agents from a template' endpoint instead.", + "deprecated": true + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "Deprecated: No longer used. The id of the project the agent belongs to.", + "deprecated": true + }, + "template_id": { + "type": "string", + "title": "Template Id", + "description": "The id of the template." + }, + "base_template_id": { + "type": "string", + "title": "Base Template Id", + "description": "The id of the base template." + }, + "identity_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 45, + "minLength": 45, + "pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Identity Ids", + "description": "The ids of the identities associated with this agent." + }, + "message_buffer_autoclear": { + "type": "boolean", + "title": "Message Buffer Autoclear", + "description": "If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case.", + "default": false + }, + "enable_sleeptime": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Sleeptime", + "description": "If set to True, memory management will move to a background agent thread." + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "Deprecated: Use `model_settings` field to configure response format instead. The response format for the agent.", + "deprecated": true + }, + "timezone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The timezone of the agent (IANA format)." + }, + "max_files_open": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Files Open", + "description": "Maximum number of files that can be open at once for this agent. Setting this too high may exceed the context window, which will break the agent." + }, + "per_file_view_window_char_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Per File View Window Char Limit", + "description": "The per-file view window character limit for this agent. Setting this too high may exceed the context window, which will break the agent." + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "Deprecated: No longer used. If set to True, the agent will be hidden.", + "deprecated": true + }, + "parallel_tool_calls": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Parallel Tool Calls", + "description": "Deprecated: Use `model_settings` to configure parallel tool calls instead. If set to True, enables parallel tool calling.", + "deprecated": true + }, + "deployment_id": { + "type": "string", + "title": "Deployment Id", + "description": "The id of the deployment." + }, + "entity_id": { + "type": "string", + "title": "Entity Id", + "description": "The id of the entity within the template." + } + }, + "type": "object", + "required": [ + "template_id", + "base_template_id", + "deployment_id", + "entity_id" + ], + "title": "InternalTemplateAgentCreate", + "description": "Used for Letta Cloud" + }, + "InternalTemplateBlockCreate": { + "properties": { + "value": { + "type": "string", + "title": "Value", + "description": "Value of the block." + }, + "limit": { + "type": "integer", + "title": "Limit", + "description": "Character limit of the block.", + "default": 100000 + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "template_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Name", + "description": "Name of the block if it is a template." + }, + "is_template": { + "type": "boolean", + "title": "Is Template", + "default": false + }, + "template_id": { + "type": "string", + "title": "Template Id", + "description": "The id of the template." + }, + "base_template_id": { + "type": "string", + "title": "Base Template Id", + "description": "The id of the base template." + }, + "deployment_id": { + "type": "string", + "title": "Deployment Id", + "description": "The id of the deployment." + }, + "entity_id": { + "type": "string", + "title": "Entity Id", + "description": "The id of the entity within the template." + }, + "preserve_on_migration": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Preserve On Migration", + "description": "Preserve the block on template migration.", + "default": false + }, + "label": { + "type": "string", + "title": "Label", + "description": "Label of the block." + }, + "read_only": { + "type": "boolean", + "title": "Read Only", + "description": "Whether the agent has read-only access to the block.", + "default": false + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "Description of the block." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata of the block.", + "default": {} + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "If set to True, the block will be hidden." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "The tags to associate with the block." + } + }, + "type": "object", + "required": [ + "value", + "template_id", + "base_template_id", + "deployment_id", + "entity_id", + "label" + ], + "title": "InternalTemplateBlockCreate", + "description": "Used for Letta Cloud" + }, + "InternalTemplateGroupCreate": { + "properties": { + "agent_ids": { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array", + "title": "Agent Ids", + "description": "" + }, + "description": { + "type": "string", + "title": "Description", + "description": "" + }, + "manager_config": { + "oneOf": [ + { + "$ref": "#/components/schemas/RoundRobinManager" + }, + { + "$ref": "#/components/schemas/SupervisorManager" + }, + { + "$ref": "#/components/schemas/DynamicManager" + }, + { + "$ref": "#/components/schemas/SleeptimeManager" + }, + { + "$ref": "#/components/schemas/VoiceSleeptimeManager" + } + ], + "title": "Manager Config", + "description": "", + "default": { + "manager_type": "round_robin" + }, + "discriminator": { + "propertyName": "manager_type", + "mapping": { + "dynamic": "#/components/schemas/DynamicManager", + "round_robin": "#/components/schemas/RoundRobinManager", + "sleeptime": "#/components/schemas/SleeptimeManager", + "supervisor": "#/components/schemas/SupervisorManager", + "voice_sleeptime": "#/components/schemas/VoiceSleeptimeManager" + } + } + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The associated project id." + }, + "shared_block_ids": { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array", + "title": "Shared Block Ids", + "description": "", + "default": [], + "deprecated": true + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "If set to True, the group will be hidden." + }, + "base_template_id": { + "type": "string", + "title": "Base Template Id", + "description": "The id of the base template." + }, + "template_id": { + "type": "string", + "title": "Template Id", + "description": "The id of the template." + }, + "deployment_id": { + "type": "string", + "title": "Deployment Id", + "description": "The id of the deployment." + } + }, + "type": "object", + "required": [ + "agent_ids", + "description", + "base_template_id", + "template_id", + "deployment_id" + ], + "title": "InternalTemplateGroupCreate", + "description": "Used for Letta Cloud" + }, + "Job": { + "properties": { + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this object." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this object." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The unix timestamp of when the job was created." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the object was last updated." + }, + "status": { + "$ref": "#/components/schemas/JobStatus", + "description": "The status of the job.", + "default": "created" + }, + "completed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Completed At", + "description": "The unix timestamp of when the job was completed." + }, + "stop_reason": { + "anyOf": [ + { + "$ref": "#/components/schemas/StopReasonType" + }, + { + "type": "null" + } + ], + "description": "The reason why the job was stopped." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "The metadata of the job." + }, + "job_type": { + "$ref": "#/components/schemas/JobType", + "description": "The type of the job.", + "default": "job" + }, + "background": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Background", + "description": "Whether the job was created in background mode." + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "The agent associated with this job/run." + }, + "callback_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Callback Url", + "description": "If set, POST to this URL when the job completes." + }, + "callback_sent_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Callback Sent At", + "description": "Timestamp when the callback was last attempted." + }, + "callback_status_code": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Callback Status Code", + "description": "HTTP status code returned by the callback endpoint." + }, + "callback_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Callback Error", + "description": "Optional error message from attempting to POST the callback endpoint." + }, + "ttft_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Ttft Ns", + "description": "Time to first token for a run in nanoseconds" + }, + "total_duration_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Total Duration Ns", + "description": "Total run duration in nanoseconds" + }, + "id": { + "type": "string", + "pattern": "^(job|run)-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Job", + "examples": ["job-123e4567-e89b-12d3-a456-426614174000"] + } + }, + "additionalProperties": false, + "type": "object", + "title": "Job", + "description": "Representation of offline jobs, used for tracking status of data loading tasks (involving parsing and embedding files)." + }, + "JobStatus": { + "type": "string", + "enum": [ + "created", + "running", + "completed", + "failed", + "pending", + "cancelled", + "expired" + ], + "title": "JobStatus", + "description": "Status of the job." + }, + "JobType": { + "type": "string", + "enum": ["job", "run", "batch"], + "title": "JobType" + }, + "JsonObjectResponseFormat": { + "properties": { + "type": { + "type": "string", + "const": "json_object", + "title": "Type", + "description": "The type of the response format.", + "default": "json_object" + } + }, + "type": "object", + "title": "JsonObjectResponseFormat", + "description": "Response format for JSON object responses." + }, + "JsonSchemaResponseFormat": { + "properties": { + "type": { + "type": "string", + "const": "json_schema", + "title": "Type", + "description": "The type of the response format.", + "default": "json_schema" + }, + "json_schema": { + "additionalProperties": true, + "type": "object", + "title": "Json Schema", + "description": "The JSON schema of the response." + } + }, + "type": "object", + "required": ["json_schema"], + "title": "JsonSchemaResponseFormat", + "description": "Response format for JSON schema-based responses." + }, + "LLMConfig": { + "properties": { + "model": { + "type": "string", + "title": "Model", + "description": "LLM model name. " + }, + "display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Display Name", + "description": "A human-friendly display name for the model." + }, + "model_endpoint_type": { + "type": "string", + "enum": [ + "openai", + "anthropic", + "google_ai", + "google_vertex", + "azure", + "groq", + "ollama", + "webui", + "webui-legacy", + "lmstudio", + "lmstudio-legacy", + "lmstudio-chatcompletions", + "llamacpp", + "koboldcpp", + "vllm", + "hugging-face", + "minimax", + "mistral", + "together", + "bedrock", + "deepseek", + "xai", + "zai", + "zai_coding", + "baseten", + "fireworks", + "openrouter", + "chatgpt_oauth" + ], + "title": "Model Endpoint Type", + "description": "The endpoint type for the model." + }, + "model_endpoint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model Endpoint", + "description": "The endpoint for the model." + }, + "provider_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider Name", + "description": "The provider name for the model." + }, + "provider_category": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderCategory" + }, + { + "type": "null" + } + ], + "description": "The provider category for the model." + }, + "model_wrapper": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model Wrapper", + "description": "The wrapper for the model." + }, + "context_window": { + "type": "integer", + "title": "Context Window", + "description": "The context window size for the model." + }, + "put_inner_thoughts_in_kwargs": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Put Inner Thoughts In Kwargs", + "description": "Puts 'inner_thoughts' as a kwarg in the function call if this is set to True. This helps with function calling performance and also the generation of inner thoughts.", + "default": false + }, + "handle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Handle", + "description": "The handle for this config, in the format provider/model-name." + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature to use when generating text with the model. A higher temperature will result in more random text.", + "default": 1 + }, + "max_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Tokens", + "description": "The maximum number of tokens to generate. If not set, the model will use its default value." + }, + "enable_reasoner": { + "type": "boolean", + "title": "Enable Reasoner", + "description": "Whether or not the model should use extended thinking if it is a 'reasoning' style model", + "default": true + }, + "reasoning_effort": { + "anyOf": [ + { + "type": "string", + "enum": ["none", "minimal", "low", "medium", "high", "xhigh"] + }, + { + "type": "null" + } + ], + "title": "Reasoning Effort", + "description": "The reasoning effort to use when generating text reasoning models" + }, + "max_reasoning_tokens": { + "type": "integer", + "title": "Max Reasoning Tokens", + "description": "Configurable thinking budget for extended thinking. Used for enable_reasoner and also for Google Vertex models like Gemini 2.5 Flash. Minimum value is 1024 when used with enable_reasoner.", + "default": 0 + }, + "effort": { + "anyOf": [ + { + "type": "string", + "enum": ["low", "medium", "high", "max"] + }, + { + "type": "null" + } + ], + "title": "Effort", + "description": "The effort level for Anthropic models that support it (Opus 4.5, Opus 4.6). Controls token spending and thinking behavior. Not setting this gives similar performance to 'high'." + }, + "frequency_penalty": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Frequency Penalty", + "description": "Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. From OpenAI: Number between -2.0 and 2.0." + }, + "compatibility_type": { + "anyOf": [ + { + "type": "string", + "enum": ["gguf", "mlx"] + }, + { + "type": "null" + } + ], + "title": "Compatibility Type", + "description": "The framework compatibility type for the model." + }, + "verbosity": { + "anyOf": [ + { + "type": "string", + "enum": ["low", "medium", "high"] + }, + { + "type": "null" + } + ], + "title": "Verbosity", + "description": "Soft control for how verbose model output should be, used for GPT-5 models." + }, + "tier": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tier", + "description": "The cost tier for the model (cloud only)." + }, + "parallel_tool_calls": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Parallel Tool Calls", + "description": "Deprecated: Use model_settings to configure parallel tool calls instead. If set to True, enables parallel tool calling. Defaults to False.", + "default": false, + "deprecated": true + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model's output. Supports text, json_object, and json_schema (structured outputs). Can be set via model_settings." + }, + "strict": { + "type": "boolean", + "title": "Strict", + "description": "Enable strict mode for tool calling. When true, tool schemas include strict: true and additionalProperties: false, guaranteeing tool outputs match JSON schemas.", + "default": false + }, + "return_logprobs": { + "type": "boolean", + "title": "Return Logprobs", + "description": "Whether to return log probabilities of the output tokens. Useful for RL training.", + "default": false + }, + "top_logprobs": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Top Logprobs", + "description": "Number of most likely tokens to return at each position (0-20). Requires return_logprobs=True." + }, + "return_token_ids": { + "type": "boolean", + "title": "Return Token Ids", + "description": "Whether to return token IDs for all LLM generations via SGLang native endpoint. Required for multi-turn RL training with loss masking. Only works with SGLang provider.", + "default": false + }, + "tool_call_parser": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tool Call Parser", + "description": "SGLang tool call parser name (e.g. 'glm47', 'qwen25', 'hermes'). Used by the SGLang native adapter to parse tool calls from raw model output." + } + }, + "type": "object", + "required": ["model", "model_endpoint_type", "context_window"], + "title": "LLMConfig", + "description": "Configuration for Language Model (LLM) connection and generation parameters.\n\n.. deprecated::\n LLMConfig is deprecated and should not be used as an input or return type in API calls.\n Use the schemas in letta.schemas.model (ModelSettings, OpenAIModelSettings, etc.) instead.\n For conversion, use the _to_model() method or Model._from_llm_config() method." + }, + "LettaAsyncRequest": { + "properties": { + "messages": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageCreate" + }, + { + "$ref": "#/components/schemas/ApprovalCreate" + }, + { + "$ref": "#/components/schemas/ToolReturnCreate" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Messages", + "description": "The messages to be sent to the agent." + }, + "input": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "$ref": "#/components/schemas/ToolCallContent" + }, + { + "$ref": "#/components/schemas/ToolReturnContent" + }, + { + "$ref": "#/components/schemas/ReasoningContent" + }, + { + "$ref": "#/components/schemas/RedactedReasoningContent" + }, + { + "$ref": "#/components/schemas/OmittedReasoningContent" + }, + { + "$ref": "#/components/schemas/SummarizedReasoningContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "image": "#/components/schemas/ImageContent", + "omitted_reasoning": "#/components/schemas/OmittedReasoningContent", + "reasoning": "#/components/schemas/ReasoningContent", + "redacted_reasoning": "#/components/schemas/RedactedReasoningContent", + "summarized_reasoning": "#/components/schemas/SummarizedReasoningContent", + "text": "#/components/schemas/TextContent", + "tool_call": "#/components/schemas/ToolCallContent", + "tool_return": "#/components/schemas/ToolReturnContent" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Input", + "description": "Syntactic sugar for a single user message. Equivalent to messages=[{'role': 'user', 'content': input}]." + }, + "max_steps": { + "type": "integer", + "title": "Max Steps", + "description": "Maximum number of steps the agent should take to process the request.", + "default": 50 + }, + "use_assistant_message": { + "type": "boolean", + "title": "Use Assistant Message", + "description": "Whether the server should parse specific tool call arguments (default `send_message`) as `AssistantMessage` objects. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": true, + "deprecated": true + }, + "assistant_message_tool_name": { + "type": "string", + "title": "Assistant Message Tool Name", + "description": "The name of the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": "send_message", + "deprecated": true + }, + "assistant_message_tool_kwarg": { + "type": "string", + "title": "Assistant Message Tool Kwarg", + "description": "The name of the message argument in the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": "message", + "deprecated": true + }, + "include_return_message_types": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MessageType" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Include Return Message Types", + "description": "Only return specified message types in the response. If `None` (default) returns all messages." + }, + "enable_thinking": { + "type": "string", + "title": "Enable Thinking", + "description": "If set to True, enables reasoning before responses or tool calls from the agent.", + "default": true, + "deprecated": true + }, + "client_tools": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ClientToolSchema" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Client Tools", + "description": "Client-side tools that the agent can call. When the agent calls a client-side tool, execution pauses and returns control to the client to execute the tool and provide the result via a ToolReturn." + }, + "client_skills": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ClientSkillSchema" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Client Skills", + "description": "Client-side skills available in the environment. These are rendered in the system prompt's available skills section alongside agent-scoped skills from MemFS." + }, + "override_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override Model", + "description": "Model handle to use for this request instead of the agent's default model. This allows sending a message to a different model without changing the agent's configuration." + }, + "include_compaction_messages": { + "type": "boolean", + "title": "Include Compaction Messages", + "description": "If True, compaction events emit structured `SummaryMessage` and `EventMessage` types. If False (default), compaction messages are not included in the response.", + "default": false + }, + "return_logprobs": { + "type": "boolean", + "title": "Return Logprobs", + "description": "If True, returns log probabilities of the output tokens in the response. Useful for RL training. Only supported for OpenAI-compatible providers (including SGLang).", + "default": false + }, + "top_logprobs": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Top Logprobs", + "description": "Number of most likely tokens to return at each position (0-20). Requires return_logprobs=True." + }, + "return_token_ids": { + "type": "boolean", + "title": "Return Token Ids", + "description": "If True, returns token IDs and logprobs for ALL LLM generations in the agent step, not just the last one. Uses SGLang native /generate endpoint. Returns 'turns' field with TurnTokenData for each assistant/tool turn. Required for proper multi-turn RL training with loss masking.", + "default": false + }, + "override_system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override System", + "description": "Optional per-request system prompt override. When set, this is passed directly to the underlying LLM request and bypasses the persisted/compiled system message for that request." + }, + "callback_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Callback Url", + "description": "Optional callback URL to POST to when the job completes" + } + }, + "type": "object", + "title": "LettaAsyncRequest" + }, + "LettaBatchMessages": { + "properties": { + "messages": { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array", + "title": "Messages" + } + }, + "type": "object", + "required": ["messages"], + "title": "LettaBatchMessages" + }, + "LettaBatchRequest": { + "properties": { + "messages": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageCreate" + }, + { + "$ref": "#/components/schemas/ApprovalCreate" + }, + { + "$ref": "#/components/schemas/ToolReturnCreate" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Messages", + "description": "The messages to be sent to the agent." + }, + "input": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "$ref": "#/components/schemas/ToolCallContent" + }, + { + "$ref": "#/components/schemas/ToolReturnContent" + }, + { + "$ref": "#/components/schemas/ReasoningContent" + }, + { + "$ref": "#/components/schemas/RedactedReasoningContent" + }, + { + "$ref": "#/components/schemas/OmittedReasoningContent" + }, + { + "$ref": "#/components/schemas/SummarizedReasoningContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "image": "#/components/schemas/ImageContent", + "omitted_reasoning": "#/components/schemas/OmittedReasoningContent", + "reasoning": "#/components/schemas/ReasoningContent", + "redacted_reasoning": "#/components/schemas/RedactedReasoningContent", + "summarized_reasoning": "#/components/schemas/SummarizedReasoningContent", + "text": "#/components/schemas/TextContent", + "tool_call": "#/components/schemas/ToolCallContent", + "tool_return": "#/components/schemas/ToolReturnContent" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Input", + "description": "Syntactic sugar for a single user message. Equivalent to messages=[{'role': 'user', 'content': input}]." + }, + "max_steps": { + "type": "integer", + "title": "Max Steps", + "description": "Maximum number of steps the agent should take to process the request.", + "default": 50 + }, + "use_assistant_message": { + "type": "boolean", + "title": "Use Assistant Message", + "description": "Whether the server should parse specific tool call arguments (default `send_message`) as `AssistantMessage` objects. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": true, + "deprecated": true + }, + "assistant_message_tool_name": { + "type": "string", + "title": "Assistant Message Tool Name", + "description": "The name of the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": "send_message", + "deprecated": true + }, + "assistant_message_tool_kwarg": { + "type": "string", + "title": "Assistant Message Tool Kwarg", + "description": "The name of the message argument in the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": "message", + "deprecated": true + }, + "include_return_message_types": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MessageType" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Include Return Message Types", + "description": "Only return specified message types in the response. If `None` (default) returns all messages." + }, + "enable_thinking": { + "type": "string", + "title": "Enable Thinking", + "description": "If set to True, enables reasoning before responses or tool calls from the agent.", + "default": true, + "deprecated": true + }, + "client_tools": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ClientToolSchema" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Client Tools", + "description": "Client-side tools that the agent can call. When the agent calls a client-side tool, execution pauses and returns control to the client to execute the tool and provide the result via a ToolReturn." + }, + "client_skills": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ClientSkillSchema" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Client Skills", + "description": "Client-side skills available in the environment. These are rendered in the system prompt's available skills section alongside agent-scoped skills from MemFS." + }, + "override_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override Model", + "description": "Model handle to use for this request instead of the agent's default model. This allows sending a message to a different model without changing the agent's configuration." + }, + "include_compaction_messages": { + "type": "boolean", + "title": "Include Compaction Messages", + "description": "If True, compaction events emit structured `SummaryMessage` and `EventMessage` types. If False (default), compaction messages are not included in the response.", + "default": false + }, + "return_logprobs": { + "type": "boolean", + "title": "Return Logprobs", + "description": "If True, returns log probabilities of the output tokens in the response. Useful for RL training. Only supported for OpenAI-compatible providers (including SGLang).", + "default": false + }, + "top_logprobs": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Top Logprobs", + "description": "Number of most likely tokens to return at each position (0-20). Requires return_logprobs=True." + }, + "return_token_ids": { + "type": "boolean", + "title": "Return Token Ids", + "description": "If True, returns token IDs and logprobs for ALL LLM generations in the agent step, not just the last one. Uses SGLang native /generate endpoint. Returns 'turns' field with TurnTokenData for each assistant/tool turn. Required for proper multi-turn RL training with loss masking.", + "default": false + }, + "override_system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override System", + "description": "Optional per-request system prompt override. When set, this is passed directly to the underlying LLM request and bypasses the persisted/compiled system message for that request." + }, + "agent_id": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "title": "Agent Id", + "description": "The ID of the agent to send this batch request for", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + } + }, + "type": "object", + "required": ["agent_id"], + "title": "LettaBatchRequest" + }, + "LettaErrorMessage": { + "properties": { + "message_type": { + "type": "string", + "const": "error_message", + "title": "Message Type", + "description": "The type of the message.", + "default": "error_message" + }, + "run_id": { + "type": "string", + "title": "Run ID", + "description": "The ID of the run." + }, + "error_type": { + "type": "string", + "title": "Error Type", + "description": "The type of error." + }, + "message": { + "type": "string", + "title": "Message", + "description": "The error message." + }, + "detail": { + "type": "string", + "title": "Detail", + "description": "An optional error detail." + }, + "seq_id": { + "type": "integer", + "title": "Seq ID", + "description": "The sequence ID for cursor-based pagination." + } + }, + "type": "object", + "required": ["message_type", "run_id", "error_type", "message"], + "title": "LettaErrorMessage", + "description": "Error messages are used to notify the client of an error that occurred during the agent's execution." + }, + "LettaImage": { + "properties": { + "type": { + "type": "string", + "const": "letta", + "title": "Type", + "description": "The source type for the image.", + "default": "letta" + }, + "file_id": { + "type": "string", + "title": "File Id", + "description": "The unique identifier of the image file persisted in storage." + }, + "media_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Media Type", + "description": "The media type for the image." + }, + "data": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Data", + "description": "The base64 encoded image data." + }, + "detail": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Detail", + "description": "What level of detail to use when processing and understanding the image (low, high, or auto to let the model decide)" + } + }, + "type": "object", + "required": ["file_id"], + "title": "LettaImage" + }, + "LettaPing": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "message_type": { + "type": "string", + "const": "ping", + "title": "Message Type", + "description": "The type of the message. Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.", + "default": "ping" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err" + }, + "seq_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seq Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + } + }, + "type": "object", + "required": ["id", "date"], + "title": "LettaPing", + "description": "A ping message used as a keepalive to prevent SSE streams from timing out during long running requests.\n\nArgs:\n id (str): The ID of the message\n date (datetime): The date the message was created in ISO format" + }, + "LettaRequest": { + "properties": { + "messages": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageCreate" + }, + { + "$ref": "#/components/schemas/ApprovalCreate" + }, + { + "$ref": "#/components/schemas/ToolReturnCreate" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Messages", + "description": "The messages to be sent to the agent." + }, + "input": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "$ref": "#/components/schemas/ToolCallContent" + }, + { + "$ref": "#/components/schemas/ToolReturnContent" + }, + { + "$ref": "#/components/schemas/ReasoningContent" + }, + { + "$ref": "#/components/schemas/RedactedReasoningContent" + }, + { + "$ref": "#/components/schemas/OmittedReasoningContent" + }, + { + "$ref": "#/components/schemas/SummarizedReasoningContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "image": "#/components/schemas/ImageContent", + "omitted_reasoning": "#/components/schemas/OmittedReasoningContent", + "reasoning": "#/components/schemas/ReasoningContent", + "redacted_reasoning": "#/components/schemas/RedactedReasoningContent", + "summarized_reasoning": "#/components/schemas/SummarizedReasoningContent", + "text": "#/components/schemas/TextContent", + "tool_call": "#/components/schemas/ToolCallContent", + "tool_return": "#/components/schemas/ToolReturnContent" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Input", + "description": "Syntactic sugar for a single user message. Equivalent to messages=[{'role': 'user', 'content': input}]." + }, + "max_steps": { + "type": "integer", + "title": "Max Steps", + "description": "Maximum number of steps the agent should take to process the request.", + "default": 50 + }, + "use_assistant_message": { + "type": "boolean", + "title": "Use Assistant Message", + "description": "Whether the server should parse specific tool call arguments (default `send_message`) as `AssistantMessage` objects. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": true, + "deprecated": true + }, + "assistant_message_tool_name": { + "type": "string", + "title": "Assistant Message Tool Name", + "description": "The name of the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": "send_message", + "deprecated": true + }, + "assistant_message_tool_kwarg": { + "type": "string", + "title": "Assistant Message Tool Kwarg", + "description": "The name of the message argument in the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": "message", + "deprecated": true + }, + "include_return_message_types": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MessageType" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Include Return Message Types", + "description": "Only return specified message types in the response. If `None` (default) returns all messages." + }, + "enable_thinking": { + "type": "string", + "title": "Enable Thinking", + "description": "If set to True, enables reasoning before responses or tool calls from the agent.", + "default": true, + "deprecated": true + }, + "client_tools": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ClientToolSchema" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Client Tools", + "description": "Client-side tools that the agent can call. When the agent calls a client-side tool, execution pauses and returns control to the client to execute the tool and provide the result via a ToolReturn." + }, + "client_skills": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ClientSkillSchema" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Client Skills", + "description": "Client-side skills available in the environment. These are rendered in the system prompt's available skills section alongside agent-scoped skills from MemFS." + }, + "override_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override Model", + "description": "Model handle to use for this request instead of the agent's default model. This allows sending a message to a different model without changing the agent's configuration." + }, + "include_compaction_messages": { + "type": "boolean", + "title": "Include Compaction Messages", + "description": "If True, compaction events emit structured `SummaryMessage` and `EventMessage` types. If False (default), compaction messages are not included in the response.", + "default": false + }, + "return_logprobs": { + "type": "boolean", + "title": "Return Logprobs", + "description": "If True, returns log probabilities of the output tokens in the response. Useful for RL training. Only supported for OpenAI-compatible providers (including SGLang).", + "default": false + }, + "top_logprobs": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Top Logprobs", + "description": "Number of most likely tokens to return at each position (0-20). Requires return_logprobs=True." + }, + "return_token_ids": { + "type": "boolean", + "title": "Return Token Ids", + "description": "If True, returns token IDs and logprobs for ALL LLM generations in the agent step, not just the last one. Uses SGLang native /generate endpoint. Returns 'turns' field with TurnTokenData for each assistant/tool turn. Required for proper multi-turn RL training with loss masking.", + "default": false + }, + "override_system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override System", + "description": "Optional per-request system prompt override. When set, this is passed directly to the underlying LLM request and bypasses the persisted/compiled system message for that request." + } + }, + "type": "object", + "title": "LettaRequest" + }, + "LettaRequestConfig": { + "properties": { + "use_assistant_message": { + "type": "boolean", + "title": "Use Assistant Message", + "description": "Whether the server should parse specific tool call arguments (default `send_message`) as `AssistantMessage` objects.", + "default": true + }, + "assistant_message_tool_name": { + "type": "string", + "title": "Assistant Message Tool Name", + "description": "The name of the designated message tool.", + "default": "send_message" + }, + "assistant_message_tool_kwarg": { + "type": "string", + "title": "Assistant Message Tool Kwarg", + "description": "The name of the message argument in the designated message tool.", + "default": "message" + }, + "include_return_message_types": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MessageType" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Include Return Message Types", + "description": "Only return specified message types in the response. If `None` (default) returns all messages." + } + }, + "type": "object", + "title": "LettaRequestConfig" + }, + "LettaResponse": { + "properties": { + "messages": { + "items": { + "$ref": "#/components/schemas/LettaMessageUnion" + }, + "type": "array", + "title": "Messages", + "description": "The messages returned by the agent." + }, + "stop_reason": { + "$ref": "#/components/schemas/LettaStopReason" + }, + "usage": { + "$ref": "#/components/schemas/LettaUsageStatistics", + "description": "The usage statistics of the agent." + }, + "logprobs": { + "anyOf": [ + { + "$ref": "#/components/schemas/letta__schemas__openai__chat_completion_response__ChoiceLogprobs" + }, + { + "type": "null" + } + ], + "description": "Log probabilities of the output tokens from the last LLM call. Only present if return_logprobs was enabled." + }, + "turns": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/TurnTokenData" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Turns", + "description": "Token data for all LLM generations in multi-turn agent interaction. Includes token IDs and logprobs for each assistant turn, plus tool result content. Only present if return_token_ids was enabled. Used for RL training with loss masking." + } + }, + "type": "object", + "required": ["messages", "stop_reason", "usage"], + "title": "LettaResponse", + "description": "Response object from an agent interaction, consisting of the new messages generated by the agent and usage statistics.\nThe type of the returned messages can be either `Message` or `LettaMessage`, depending on what was specified in the request.\n\nAttributes:\n messages (List[Union[Message, LettaMessage]]): The messages returned by the agent.\n usage (LettaUsageStatistics): The usage statistics" + }, + "LettaStopReason": { + "properties": { + "message_type": { + "type": "string", + "const": "stop_reason", + "title": "Message Type", + "description": "The type of the message.", + "default": "stop_reason" + }, + "stop_reason": { + "$ref": "#/components/schemas/StopReasonType", + "description": "The reason why execution stopped." + } + }, + "type": "object", + "required": ["stop_reason"], + "title": "LettaStopReason", + "description": "The stop reason from Letta indicating why agent loop stopped execution." + }, + "LettaStreamingRequest": { + "properties": { + "messages": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageCreate" + }, + { + "$ref": "#/components/schemas/ApprovalCreate" + }, + { + "$ref": "#/components/schemas/ToolReturnCreate" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Messages", + "description": "The messages to be sent to the agent." + }, + "input": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "$ref": "#/components/schemas/ToolCallContent" + }, + { + "$ref": "#/components/schemas/ToolReturnContent" + }, + { + "$ref": "#/components/schemas/ReasoningContent" + }, + { + "$ref": "#/components/schemas/RedactedReasoningContent" + }, + { + "$ref": "#/components/schemas/OmittedReasoningContent" + }, + { + "$ref": "#/components/schemas/SummarizedReasoningContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "image": "#/components/schemas/ImageContent", + "omitted_reasoning": "#/components/schemas/OmittedReasoningContent", + "reasoning": "#/components/schemas/ReasoningContent", + "redacted_reasoning": "#/components/schemas/RedactedReasoningContent", + "summarized_reasoning": "#/components/schemas/SummarizedReasoningContent", + "text": "#/components/schemas/TextContent", + "tool_call": "#/components/schemas/ToolCallContent", + "tool_return": "#/components/schemas/ToolReturnContent" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Input", + "description": "Syntactic sugar for a single user message. Equivalent to messages=[{'role': 'user', 'content': input}]." + }, + "max_steps": { + "type": "integer", + "title": "Max Steps", + "description": "Maximum number of steps the agent should take to process the request.", + "default": 50 + }, + "use_assistant_message": { + "type": "boolean", + "title": "Use Assistant Message", + "description": "Whether the server should parse specific tool call arguments (default `send_message`) as `AssistantMessage` objects. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": true, + "deprecated": true + }, + "assistant_message_tool_name": { + "type": "string", + "title": "Assistant Message Tool Name", + "description": "The name of the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": "send_message", + "deprecated": true + }, + "assistant_message_tool_kwarg": { + "type": "string", + "title": "Assistant Message Tool Kwarg", + "description": "The name of the message argument in the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.", + "default": "message", + "deprecated": true + }, + "include_return_message_types": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MessageType" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Include Return Message Types", + "description": "Only return specified message types in the response. If `None` (default) returns all messages." + }, + "enable_thinking": { + "type": "string", + "title": "Enable Thinking", + "description": "If set to True, enables reasoning before responses or tool calls from the agent.", + "default": true, + "deprecated": true + }, + "client_tools": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ClientToolSchema" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Client Tools", + "description": "Client-side tools that the agent can call. When the agent calls a client-side tool, execution pauses and returns control to the client to execute the tool and provide the result via a ToolReturn." + }, + "client_skills": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ClientSkillSchema" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Client Skills", + "description": "Client-side skills available in the environment. These are rendered in the system prompt's available skills section alongside agent-scoped skills from MemFS." + }, + "override_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override Model", + "description": "Model handle to use for this request instead of the agent's default model. This allows sending a message to a different model without changing the agent's configuration." + }, + "include_compaction_messages": { + "type": "boolean", + "title": "Include Compaction Messages", + "description": "If True, compaction events emit structured `SummaryMessage` and `EventMessage` types. If False (default), compaction messages are not included in the response.", + "default": false + }, + "return_logprobs": { + "type": "boolean", + "title": "Return Logprobs", + "description": "If True, returns log probabilities of the output tokens in the response. Useful for RL training. Only supported for OpenAI-compatible providers (including SGLang).", + "default": false + }, + "top_logprobs": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Top Logprobs", + "description": "Number of most likely tokens to return at each position (0-20). Requires return_logprobs=True." + }, + "return_token_ids": { + "type": "boolean", + "title": "Return Token Ids", + "description": "If True, returns token IDs and logprobs for ALL LLM generations in the agent step, not just the last one. Uses SGLang native /generate endpoint. Returns 'turns' field with TurnTokenData for each assistant/tool turn. Required for proper multi-turn RL training with loss masking.", + "default": false + }, + "override_system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Override System", + "description": "Optional per-request system prompt override. When set, this is passed directly to the underlying LLM request and bypasses the persisted/compiled system message for that request." + }, + "streaming": { + "type": "boolean", + "title": "Streaming", + "description": "If True, returns a streaming response (Server-Sent Events). If False (default), returns a complete response.", + "default": false + }, + "stream_tokens": { + "type": "boolean", + "title": "Stream Tokens", + "description": "Flag to determine if individual tokens should be streamed, rather than streaming per step (only used when streaming=true).", + "default": false + }, + "include_pings": { + "type": "boolean", + "title": "Include Pings", + "description": "Whether to include periodic keepalive ping messages in the stream to prevent connection timeouts (only used when streaming=true).", + "default": true + }, + "background": { + "type": "boolean", + "title": "Background", + "description": "Whether to process the request in the background (only used when streaming=true).", + "default": false + } + }, + "type": "object", + "title": "LettaStreamingRequest" + }, + "LettaStreamingResponse": { + "oneOf": [ + { + "$ref": "#/components/schemas/SystemMessage" + }, + { + "$ref": "#/components/schemas/UserMessage" + }, + { + "$ref": "#/components/schemas/ReasoningMessage" + }, + { + "$ref": "#/components/schemas/HiddenReasoningMessage" + }, + { + "$ref": "#/components/schemas/ToolCallMessage" + }, + { + "$ref": "#/components/schemas/ToolReturnMessage" + }, + { + "$ref": "#/components/schemas/AssistantMessage" + }, + { + "$ref": "#/components/schemas/ApprovalRequestMessage" + }, + { + "$ref": "#/components/schemas/ApprovalResponseMessage" + }, + { + "$ref": "#/components/schemas/LettaPing" + }, + { + "$ref": "#/components/schemas/LettaErrorMessage" + }, + { + "$ref": "#/components/schemas/LettaStopReason" + }, + { + "$ref": "#/components/schemas/LettaUsageStatistics" + } + ], + "title": "LettaStreamingResponse", + "description": "Streaming response type for Server-Sent Events (SSE) endpoints.\nEach event in the stream will be one of these types.", + "discriminator": { + "propertyName": "message_type", + "mapping": { + "approval_request_message": "#/components/schemas/ApprovalRequestMessage", + "approval_response_message": "#/components/schemas/ApprovalResponseMessage", + "assistant_message": "#/components/schemas/AssistantMessage", + "error_message": "#/components/schemas/LettaErrorMessage", + "hidden_reasoning_message": "#/components/schemas/HiddenReasoningMessage", + "ping": "#/components/schemas/LettaPing", + "reasoning_message": "#/components/schemas/ReasoningMessage", + "stop_reason": "#/components/schemas/LettaStopReason", + "system_message": "#/components/schemas/SystemMessage", + "tool_call_message": "#/components/schemas/ToolCallMessage", + "tool_return_message": "#/components/schemas/ToolReturnMessage", + "usage_statistics": "#/components/schemas/LettaUsageStatistics", + "user_message": "#/components/schemas/UserMessage" + } + } + }, + "LettaUsageStatistics": { + "properties": { + "message_type": { + "type": "string", + "const": "usage_statistics", + "title": "Message Type", + "default": "usage_statistics" + }, + "completion_tokens": { + "type": "integer", + "title": "Completion Tokens", + "description": "The number of tokens generated by the agent.", + "default": 0 + }, + "prompt_tokens": { + "type": "integer", + "title": "Prompt Tokens", + "description": "The number of tokens in the prompt.", + "default": 0 + }, + "total_tokens": { + "type": "integer", + "title": "Total Tokens", + "description": "The total number of tokens processed by the agent.", + "default": 0 + }, + "step_count": { + "type": "integer", + "title": "Step Count", + "description": "The number of steps taken by the agent.", + "default": 0 + }, + "run_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Run Ids", + "description": "The background task run IDs associated with the agent interaction" + }, + "cached_input_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Cached Input Tokens", + "description": "The number of input tokens served from cache. None if not reported by provider." + }, + "cache_write_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Cache Write Tokens", + "description": "The number of input tokens written to cache (Anthropic only). None if not reported by provider." + }, + "reasoning_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Reasoning Tokens", + "description": "The number of reasoning/thinking tokens generated. None if not reported by provider." + }, + "context_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Context Tokens", + "description": "Estimate of tokens currently in the context window." + } + }, + "type": "object", + "title": "LettaUsageStatistics", + "description": "Usage statistics for the agent interaction.\n\nAttributes:\n completion_tokens (int): The number of tokens generated by the agent.\n prompt_tokens (int): The number of tokens in the prompt.\n total_tokens (int): The total number of tokens processed by the agent.\n step_count (int): The number of steps taken by the agent.\n cached_input_tokens (Optional[int]): The number of input tokens served from cache. None if not reported.\n cache_write_tokens (Optional[int]): The number of input tokens written to cache. None if not reported.\n reasoning_tokens (Optional[int]): The number of reasoning/thinking tokens generated. None if not reported." + }, + "ListDeploymentEntitiesResponse": { + "properties": { + "entities": { + "items": { + "$ref": "#/components/schemas/DeploymentEntity" + }, + "type": "array", + "title": "Entities", + "default": [] + }, + "total_count": { + "type": "integer", + "title": "Total Count" + }, + "deployment_id": { + "type": "string", + "title": "Deployment Id" + }, + "message": { + "type": "string", + "title": "Message" + } + }, + "type": "object", + "required": ["total_count", "deployment_id", "message"], + "title": "ListDeploymentEntitiesResponse", + "description": "Response model for listing deployment entities." + }, + "LocalSandboxConfig": { + "properties": { + "sandbox_dir": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sandbox Dir", + "description": "Directory for the sandbox environment." + }, + "use_venv": { + "type": "boolean", + "title": "Use Venv", + "description": "Whether or not to use the venv, or run directly in the same run loop.", + "default": false + }, + "venv_name": { + "type": "string", + "title": "Venv Name", + "description": "The name for the venv in the sandbox directory. We first search for an existing venv with this name, otherwise, we make it from the requirements.txt.", + "default": "venv" + }, + "pip_requirements": { + "items": { + "$ref": "#/components/schemas/PipRequirement" + }, + "type": "array", + "title": "Pip Requirements", + "description": "List of pip packages to install with mandatory name and optional version following semantic versioning. This only is considered when use_venv is True." + } + }, + "type": "object", + "title": "LocalSandboxConfig" + }, + "MCPServerSchema": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Human-readable MCP server ID" + }, + "server_type": { + "type": "string", + "title": "Server Type" + }, + "server_name": { + "type": "string", + "title": "Server Name" + }, + "server_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Server Url" + }, + "stdio_config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Stdio Config" + }, + "metadata_": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" + } + }, + "type": "object", + "required": ["id", "server_type", "server_name"], + "title": "MCPServerSchema", + "description": "MCP server schema for agent files with remapped ID." + }, + "MCPServerType": { + "type": "string", + "enum": ["sse", "stdio", "streamable_http"], + "title": "MCPServerType" + }, + "MCPTool": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "inputSchema": { + "additionalProperties": true, + "type": "object", + "title": "Inputschema" + }, + "outputSchema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Outputschema" + }, + "annotations": { + "anyOf": [ + { + "$ref": "#/components/schemas/ToolAnnotations" + }, + { + "type": "null" + } + ] + }, + "_meta": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Meta" + }, + "health": { + "anyOf": [ + { + "$ref": "#/components/schemas/MCPToolHealth" + }, + { + "type": "null" + } + ], + "description": "Schema health status for OpenAI strict mode" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["name", "inputSchema"], + "title": "MCPTool", + "description": "A simple wrapper around MCP's tool definition (to avoid conflict with our own)" + }, + "MCPToolHealth": { + "properties": { + "status": { + "type": "string", + "title": "Status", + "description": "Schema health status: STRICT_COMPLIANT, NON_STRICT_ONLY, or INVALID" + }, + "reasons": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Reasons", + "description": "List of reasons for the health status" + } + }, + "type": "object", + "required": ["status"], + "title": "MCPToolHealth", + "description": "Health status for an MCP tool's schema." + }, + "ManagerType": { + "type": "string", + "enum": [ + "round_robin", + "supervisor", + "dynamic", + "sleeptime", + "voice_sleeptime", + "swarm" + ], + "title": "ManagerType" + }, + "MaxCountPerStepToolRule": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name", + "description": "The name of the tool. Must exist in the database for the user's organization." + }, + "type": { + "type": "string", + "const": "max_count_per_step", + "title": "Type", + "default": "max_count_per_step" + }, + "prompt_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt Template", + "description": "Optional template string (ignored)." + }, + "max_count_limit": { + "type": "integer", + "title": "Max Count Limit", + "description": "The max limit for the total number of times this tool can be invoked in a single step." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["tool_name", "max_count_limit"], + "title": "MaxCountPerStepToolRule", + "description": "Represents a tool rule configuration which constrains the total number of times this tool can be invoked in a single step." + }, + "MaxCountPerStepToolRuleSchema": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name" + }, + "type": { + "type": "string", + "title": "Type" + }, + "max_count_limit": { + "type": "integer", + "title": "Max Count Limit" + } + }, + "type": "object", + "required": ["tool_name", "type", "max_count_limit"], + "title": "MaxCountPerStepToolRuleSchema" + }, + "Memory": { + "properties": { + "agent_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentType" + }, + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Type", + "description": "Agent type controlling prompt rendering." + }, + "git_enabled": { + "type": "boolean", + "title": "Git Enabled", + "description": "Whether this agent uses git-backed memory with structured labels.", + "default": false + }, + "blocks": { + "items": { + "$ref": "#/components/schemas/Block" + }, + "type": "array", + "title": "Blocks", + "description": "Memory blocks contained in the agent's in-context memory" + }, + "file_blocks": { + "items": { + "$ref": "#/components/schemas/FileBlock" + }, + "type": "array", + "title": "File Blocks", + "description": "Special blocks representing the agent's in-context memory of an attached file" + }, + "prompt_template": { + "type": "string", + "title": "Prompt Template", + "description": "Deprecated. Ignored for performance.", + "default": "" + } + }, + "type": "object", + "required": ["blocks"], + "title": "Memory", + "description": "Represents the in-context memory (i.e. Core memory) of the agent. This includes both the `Block` objects (labelled by sections), as well as tools to edit the blocks." + }, + "Message": { + "properties": { + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this object." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this object." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The timestamp when the object was created." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the object was last updated." + }, + "id": { + "type": "string", + "pattern": "^message-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Message", + "examples": ["message-123e4567-e89b-12d3-a456-426614174000"] + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "The unique identifier of the agent." + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "The model used to make the function call." + }, + "role": { + "$ref": "#/components/schemas/MessageRole", + "description": "The role of the participant." + }, + "content": { + "anyOf": [ + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "$ref": "#/components/schemas/ToolCallContent" + }, + { + "$ref": "#/components/schemas/ToolReturnContent" + }, + { + "$ref": "#/components/schemas/ReasoningContent" + }, + { + "$ref": "#/components/schemas/RedactedReasoningContent" + }, + { + "$ref": "#/components/schemas/OmittedReasoningContent" + }, + { + "$ref": "#/components/schemas/SummarizedReasoningContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "image": "#/components/schemas/ImageContent", + "omitted_reasoning": "#/components/schemas/OmittedReasoningContent", + "reasoning": "#/components/schemas/ReasoningContent", + "redacted_reasoning": "#/components/schemas/RedactedReasoningContent", + "summarized_reasoning": "#/components/schemas/SummarizedReasoningContent", + "text": "#/components/schemas/TextContent", + "tool_call": "#/components/schemas/ToolCallContent", + "tool_return": "#/components/schemas/ToolReturnContent" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Content", + "description": "The content of the message." + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "For role user/assistant: the (optional) name of the participant. For role tool/function: the name of the function called." + }, + "tool_calls": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ChatCompletionMessageFunctionToolCall-Output" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Calls", + "description": "The list of tool calls requested. Only applicable for role assistant." + }, + "tool_call_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tool Call Id", + "description": "The ID of the tool call. Only applicable for role tool." + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id", + "description": "The id of the step that this message was created in." + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id", + "description": "The id of the run that this message was created in." + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id associated with this message" + }, + "tool_returns": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/letta__schemas__message__ToolReturn-Output" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Returns", + "description": "Tool execution return information for prior tool calls" + }, + "group_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Group Id", + "description": "The multi-agent group that the message was sent in" + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id", + "description": "The id of the sender of the message, can be an identity id or agent id" + }, + "batch_item_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Batch Item Id", + "description": "The id of the LLMBatchItem that this message is associated with" + }, + "conversation_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Conversation Id", + "description": "The conversation this message belongs to" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err", + "description": "Whether this message is part of an error step. Used only for debugging purposes." + }, + "approval_request_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Approval Request Id", + "description": "The id of the approval request if this message is associated with a tool call request." + }, + "approve": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Approve", + "description": "Whether tool call is approved." + }, + "denial_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Denial Reason", + "description": "The reason the tool call request was denied." + }, + "approvals": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApprovalReturn" + }, + { + "$ref": "#/components/schemas/letta__schemas__message__ToolReturn-Output" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Approvals", + "description": "The list of approvals for this message." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["role"], + "title": "Message", + "description": " Letta's internal representation of a message. Includes methods to convert to/from LLM provider formats.\n\n Attributes:\n id (str): The unique identifier of the message.\n role (MessageRole): The role of the participant.\n text (str): The text of the message.\n user_id (str): The unique identifier of the user.\n agent_id (str): The unique identifier of the agent.\n model (str): The model used to make the function call.\n name (str): The name of the participant.\n created_at (datetime): The time the message was created.\n tool_calls (List[OpenAIToolCall,]): The list of tool calls requested.\n tool_call_id (str): The id of the tool call.\n step_id (str): The id of the step that this message was created in.\n otid (str): The offline threading id associated with this message.\n tool_returns (List[ToolReturn]): The list of tool returns requested.\n group_id (str): The multi-agent group that the message was sent in.\n sender_id (str): The id of the sender of the message, can be an identity id or agent id.\n conversation_id (str): The conversation this message belongs to.\nt" + }, + "MessageCreate": { + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "message" + }, + { + "type": "null" + } + ], + "title": "Type", + "description": "The message type to be created.", + "default": "message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "group_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Group Id", + "description": "The multi-agent group that the message was sent in" + }, + "role": { + "type": "string", + "enum": ["user", "system", "assistant"], + "title": "Role", + "description": "The role of the participant." + }, + "content": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/LettaMessageContentUnion" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "title": "Content", + "description": "The content of the message." + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "The name of the participant." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id", + "description": "The id of the sender of the message, can be an identity id or agent id" + }, + "batch_item_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Batch Item Id", + "description": "The id of the LLMBatchItem that this message is associated with" + } + }, + "type": "object", + "required": ["role", "content"], + "title": "MessageCreate", + "description": "Request to create a message" + }, + "MessageRole": { + "type": "string", + "enum": [ + "assistant", + "user", + "tool", + "function", + "system", + "approval", + "summary" + ], + "title": "MessageRole" + }, + "MessageSearchCacheWarmScope": { + "properties": {}, + "additionalProperties": false, + "type": "object", + "title": "MessageSearchCacheWarmScope", + "description": "Messages currently infer scope from the authenticated actor." + }, + "MessageSearchRequest": { + "properties": { + "query": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Query", + "description": "Text query for full-text search" + }, + "search_mode": { + "type": "string", + "enum": ["vector", "fts", "hybrid"], + "title": "Search Mode", + "description": "Search mode to use", + "default": "hybrid" + }, + "roles": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MessageRole" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Roles", + "description": "Filter messages by role" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "Filter messages by agent ID" + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "Filter messages by project ID" + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "Filter messages by template ID" + }, + "conversation_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Conversation Id", + "description": "Filter messages by conversation ID" + }, + "limit": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "title": "Limit", + "description": "Maximum number of results to return", + "default": 50 + }, + "start_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Date", + "description": "Filter messages created after this date" + }, + "end_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "End Date", + "description": "Filter messages created on or before this date" + } + }, + "type": "object", + "title": "MessageSearchRequest", + "description": "Request model for searching messages across the organization" + }, + "MessageSearchResult": { + "properties": { + "embedded_text": { + "type": "string", + "title": "Embedded Text", + "description": "The embedded content (LLM-friendly)" + }, + "message": { + "$ref": "#/components/schemas/Message", + "description": "The raw message object" + }, + "fts_rank": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Fts Rank", + "description": "Full-text search rank position if FTS was used" + }, + "vector_rank": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Vector Rank", + "description": "Vector search rank position if vector search was used" + }, + "rrf_score": { + "type": "number", + "title": "Rrf Score", + "description": "Reciprocal Rank Fusion combined score" + } + }, + "type": "object", + "required": ["embedded_text", "message", "rrf_score"], + "title": "MessageSearchResult", + "description": "Result from a message search operation with scoring details." + }, + "MessageType": { + "type": "string", + "enum": [ + "system_message", + "user_message", + "assistant_message", + "reasoning_message", + "hidden_reasoning_message", + "tool_call_message", + "tool_return_message", + "approval_request_message", + "approval_response_message", + "summary_message", + "event_message" + ], + "title": "MessageType" + }, + "ModalSandboxConfig": { + "properties": { + "timeout": { + "type": "integer", + "title": "Timeout", + "description": "Time limit for the sandbox (in seconds).", + "default": 60 + }, + "pip_requirements": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Pip Requirements", + "description": "A list of pip packages to install in the Modal sandbox" + }, + "npm_requirements": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Npm Requirements", + "description": "A list of npm packages to install in the Modal sandbox" + }, + "language": { + "type": "string", + "enum": ["python", "typescript"], + "title": "Language", + "default": "python" + } + }, + "type": "object", + "title": "ModalSandboxConfig" + }, + "Model": { + "properties": { + "handle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Handle", + "description": "The handle for this config, in the format provider/model-name." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The actual model name used by the provider" + }, + "display_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Display Name", + "description": "A human-friendly display name for the model." + }, + "provider_type": { + "$ref": "#/components/schemas/ProviderType", + "description": "The type of the provider" + }, + "provider_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider Name", + "description": "The provider name for the model." + }, + "model_type": { + "type": "string", + "const": "llm", + "title": "Model Type", + "description": "Type of model (llm or embedding)", + "default": "llm" + }, + "model": { + "type": "string", + "title": "Model", + "description": "Deprecated: Use 'name' field instead. LLM model name.", + "deprecated": true + }, + "model_endpoint_type": { + "type": "string", + "enum": [ + "openai", + "anthropic", + "google_ai", + "google_vertex", + "azure", + "groq", + "ollama", + "webui", + "webui-legacy", + "lmstudio", + "lmstudio-legacy", + "lmstudio-chatcompletions", + "llamacpp", + "koboldcpp", + "vllm", + "hugging-face", + "baseten", + "minimax", + "mistral", + "together", + "bedrock", + "deepseek", + "xai", + "zai", + "zai_coding", + "openrouter", + "chatgpt_oauth" + ], + "title": "Model Endpoint Type", + "description": "Deprecated: Use 'provider_type' field instead. The endpoint type for the model.", + "deprecated": true + }, + "model_endpoint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model Endpoint", + "description": "Deprecated: The endpoint for the model.", + "deprecated": true + }, + "provider_category": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderCategory" + }, + { + "type": "null" + } + ], + "description": "Deprecated: The provider category for the model.", + "deprecated": true + }, + "model_wrapper": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model Wrapper", + "description": "Deprecated: The wrapper for the model.", + "deprecated": true + }, + "context_window": { + "type": "integer", + "title": "Context Window", + "description": "Deprecated: Use 'max_context_window' field instead. The context window size for the model.", + "deprecated": true + }, + "put_inner_thoughts_in_kwargs": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Put Inner Thoughts In Kwargs", + "description": "Deprecated: Puts 'inner_thoughts' as a kwarg in the function call.", + "default": true, + "deprecated": true + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "Deprecated: The temperature to use when generating text with the model.", + "default": 0.7, + "deprecated": true + }, + "max_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Tokens", + "description": "Deprecated: The maximum number of tokens to generate.", + "deprecated": true + }, + "enable_reasoner": { + "type": "boolean", + "title": "Enable Reasoner", + "description": "Deprecated: Whether or not the model should use extended thinking if it is a 'reasoning' style model.", + "default": true, + "deprecated": true + }, + "reasoning_effort": { + "anyOf": [ + { + "type": "string", + "enum": ["none", "minimal", "low", "medium", "high", "xhigh"] + }, + { + "type": "null" + } + ], + "title": "Reasoning Effort", + "description": "Deprecated: The reasoning effort to use when generating text reasoning models.", + "deprecated": true + }, + "max_reasoning_tokens": { + "type": "integer", + "title": "Max Reasoning Tokens", + "description": "Deprecated: Configurable thinking budget for extended thinking.", + "default": 0, + "deprecated": true + }, + "effort": { + "anyOf": [ + { + "type": "string", + "enum": ["low", "medium", "high", "max"] + }, + { + "type": "null" + } + ], + "title": "Effort", + "description": "The effort level for Anthropic models that support it (Opus 4.5, Opus 4.6). Controls token spending and thinking behavior. Not setting this gives similar performance to 'high'." + }, + "frequency_penalty": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Frequency Penalty", + "description": "Deprecated: Positive values penalize new tokens based on their existing frequency in the text so far.", + "deprecated": true + }, + "compatibility_type": { + "anyOf": [ + { + "type": "string", + "enum": ["gguf", "mlx"] + }, + { + "type": "null" + } + ], + "title": "Compatibility Type", + "description": "Deprecated: The framework compatibility type for the model.", + "deprecated": true + }, + "verbosity": { + "anyOf": [ + { + "type": "string", + "enum": ["low", "medium", "high"] + }, + { + "type": "null" + } + ], + "title": "Verbosity", + "description": "Deprecated: Soft control for how verbose model output should be.", + "deprecated": true + }, + "tier": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tier", + "description": "Deprecated: The cost tier for the model (cloud only).", + "deprecated": true + }, + "parallel_tool_calls": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Parallel Tool Calls", + "description": "Deprecated: If set to True, enables parallel tool calling.", + "default": false, + "deprecated": true + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model's output. Supports text, json_object, and json_schema (structured outputs). Can be set via model_settings." + }, + "strict": { + "type": "boolean", + "title": "Strict", + "description": "Enable strict mode for tool calling. When true, tool schemas include strict: true and additionalProperties: false, guaranteeing tool outputs match JSON schemas.", + "default": false + }, + "return_logprobs": { + "type": "boolean", + "title": "Return Logprobs", + "description": "Whether to return log probabilities of the output tokens. Useful for RL training.", + "default": false + }, + "top_logprobs": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Top Logprobs", + "description": "Number of most likely tokens to return at each position (0-20). Requires return_logprobs=True." + }, + "return_token_ids": { + "type": "boolean", + "title": "Return Token Ids", + "description": "Whether to return token IDs for all LLM generations via SGLang native endpoint. Required for multi-turn RL training with loss masking. Only works with SGLang provider.", + "default": false + }, + "tool_call_parser": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tool Call Parser", + "description": "SGLang tool call parser name (e.g. 'glm47', 'qwen25', 'hermes'). Used by the SGLang native adapter to parse tool calls from raw model output." + }, + "max_context_window": { + "type": "integer", + "title": "Max Context Window", + "description": "The maximum context window for the model" + } + }, + "type": "object", + "required": [ + "name", + "provider_type", + "model", + "model_endpoint_type", + "context_window", + "max_context_window" + ], + "title": "Model" + }, + "ModifyApprovalRequest": { + "properties": { + "requires_approval": { + "type": "boolean", + "title": "Requires Approval", + "description": "Whether the tool requires approval before execution" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["requires_approval"], + "title": "ModifyApprovalRequest", + "description": "Request body for modifying tool approval requirements." + }, + "ModifyFeedbackRequest": { + "properties": { + "feedback": { + "anyOf": [ + { + "$ref": "#/components/schemas/FeedbackType" + }, + { + "type": "null" + } + ], + "description": "Whether this feedback is positive or negative" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "Feedback tags to add to the step" + } + }, + "type": "object", + "title": "ModifyFeedbackRequest" + }, + "NpmRequirement": { + "properties": { + "name": { + "type": "string", + "minLength": 1, + "title": "Name", + "description": "Name of the npm package." + }, + "version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Version", + "description": "Optional version of the package, following semantic versioning." + } + }, + "type": "object", + "required": ["name"], + "title": "NpmRequirement" + }, + "OmittedReasoningContent": { + "properties": { + "type": { + "type": "string", + "const": "omitted_reasoning", + "title": "Type", + "description": "Indicates this is an omitted reasoning step.", + "default": "omitted_reasoning" + }, + "signature": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Signature", + "description": "A unique identifier for this reasoning step." + } + }, + "type": "object", + "title": "OmittedReasoningContent", + "description": "A placeholder for reasoning content we know is present, but isn't returned by the provider (e.g. OpenAI GPT-5 on ChatCompletions)" + }, + "OpenAIModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "openai", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "openai" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "reasoning": { + "$ref": "#/components/schemas/OpenAIReasoning", + "description": "The reasoning configuration for the model.", + "default": { + "reasoning_effort": "high" + } + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model." + }, + "strict": { + "type": "boolean", + "title": "Strict", + "description": "Enable strict mode for tool calling. When true, tool outputs are guaranteed to match JSON schemas.", + "default": true + } + }, + "type": "object", + "title": "OpenAIModelSettings" + }, + "OpenAIReasoning": { + "properties": { + "reasoning_effort": { + "type": "string", + "enum": ["none", "minimal", "low", "medium", "high", "xhigh"], + "title": "Reasoning Effort", + "description": "The reasoning effort to use when generating text reasoning models", + "default": "minimal" + } + }, + "type": "object", + "title": "OpenAIReasoning" + }, + "OpenRouterModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "openrouter", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "openrouter" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model." + } + }, + "type": "object", + "title": "OpenRouterModelSettings", + "description": "OpenRouter model configuration (OpenAI-compatible)." + }, + "Organization": { + "properties": { + "id": { + "type": "string", + "pattern": "^org-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Org", + "examples": ["org-123e4567-e89b-12d3-a456-426614174000"] + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the organization.", + "default": "SincereYogurt" + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "The creation date of the organization." + }, + "privileged_tools": { + "type": "boolean", + "title": "Privileged Tools", + "description": "Whether the organization has access to privileged tools.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "title": "Organization" + }, + "OrganizationCreate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "The name of the organization." + }, + "privileged_tools": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Privileged Tools", + "description": "Whether the organization has access to privileged tools.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "title": "OrganizationCreate" + }, + "OrganizationSourcesStats": { + "properties": { + "total_sources": { + "type": "integer", + "title": "Total Sources", + "description": "Total number of sources", + "default": 0 + }, + "total_files": { + "type": "integer", + "title": "Total Files", + "description": "Total number of files across all sources", + "default": 0 + }, + "total_size": { + "type": "integer", + "title": "Total Size", + "description": "Total size of all files in bytes", + "default": 0 + }, + "sources": { + "items": { + "$ref": "#/components/schemas/SourceStats" + }, + "type": "array", + "title": "Sources", + "description": "List of source metadata" + } + }, + "additionalProperties": false, + "type": "object", + "title": "OrganizationSourcesStats", + "description": "Complete metadata response for organization sources" + }, + "OrganizationUpdate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "The name of the organization." + }, + "privileged_tools": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Privileged Tools", + "description": "Whether the organization has access to privileged tools.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "title": "OrganizationUpdate" + }, + "PaginatedAgentFiles": { + "properties": { + "files": { + "items": { + "$ref": "#/components/schemas/AgentFileAttachment" + }, + "type": "array", + "title": "Files", + "description": "List of file attachments for the agent" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor", + "description": "Cursor for fetching the next page (file-agent relationship ID)" + }, + "has_more": { + "type": "boolean", + "title": "Has More", + "description": "Whether more results exist after this page" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["files", "has_more"], + "title": "PaginatedAgentFiles", + "description": "Paginated response for agent files" + }, + "ParameterProperties": { + "properties": { + "type": { + "type": "string", + "title": "Type" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "type": "object", + "required": ["type"], + "title": "ParameterProperties" + }, + "ParametersSchema": { + "properties": { + "type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Type", + "default": "object" + }, + "properties": { + "additionalProperties": { + "$ref": "#/components/schemas/ParameterProperties" + }, + "type": "object", + "title": "Properties" + }, + "required": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Required" + } + }, + "type": "object", + "required": ["properties"], + "title": "ParametersSchema" + }, + "ParentToolRule": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name", + "description": "The name of the tool. Must exist in the database for the user's organization." + }, + "type": { + "type": "string", + "const": "parent_last_tool", + "title": "Type", + "default": "parent_last_tool" + }, + "prompt_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt Template", + "description": "Optional template string (ignored)." + }, + "children": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Children", + "description": "The children tools that can be invoked." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["tool_name", "children"], + "title": "ParentToolRule", + "description": "A ToolRule that only allows a child tool to be called if the parent has been called." + }, + "Passage": { + "properties": { + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this object." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this object." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "The creation date of the passage." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the object was last updated." + }, + "is_deleted": { + "type": "boolean", + "title": "Is Deleted", + "description": "Whether this passage is deleted or not.", + "default": false + }, + "archive_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Archive Id", + "description": "The unique identifier of the archive containing this passage." + }, + "source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Id", + "description": "Deprecated: Use `folder_id` field instead. The data source of the passage.", + "deprecated": true + }, + "file_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Id", + "description": "The unique identifier of the file associated with the passage." + }, + "file_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File Name", + "description": "The name of the file (only for source passages)." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "The metadata of the passage.", + "default": {} + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "Tags associated with this passage." + }, + "id": { + "type": "string", + "pattern": "^passage-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Passage", + "examples": ["passage-123e4567-e89b-12d3-a456-426614174000"] + }, + "text": { + "type": "string", + "title": "Text", + "description": "The text of the passage." + }, + "embedding": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Embedding", + "description": "The embedding of the passage." + }, + "embedding_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + { + "type": "null" + } + ], + "description": "The embedding configuration used by the passage." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["text", "embedding", "embedding_config"], + "title": "Passage", + "description": "Representation of a passage, which is stored in archival memory." + }, + "PassageBatchCreateRequest": { + "properties": { + "passages": { + "items": { + "$ref": "#/components/schemas/PassageCreateRequest" + }, + "type": "array", + "title": "Passages", + "description": "Passages to create in the archive" + } + }, + "type": "object", + "required": ["passages"], + "title": "PassageBatchCreateRequest", + "description": "Request model for creating multiple passages in an archive." + }, + "PassageCreateRequest": { + "properties": { + "text": { + "type": "string", + "title": "Text", + "description": "The text content of the passage" + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Optional metadata for the passage" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "Optional tags for categorizing the passage" + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "Optional creation datetime for the passage (ISO 8601 format)" + } + }, + "type": "object", + "required": ["text"], + "title": "PassageCreateRequest", + "description": "Request model for creating a passage in an archive." + }, + "PassageSearchRequest": { + "properties": { + "query": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Query", + "description": "Text query for semantic search" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "Filter passages by agent ID" + }, + "archive_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Archive Id", + "description": "Filter passages by archive ID" + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "Optional list of tags to filter search results" + }, + "tag_match_mode": { + "type": "string", + "enum": ["any", "all"], + "title": "Tag Match Mode", + "description": "How to match tags - 'any' to match passages with any of the tags, 'all' to match only passages with all tags", + "default": "any" + }, + "limit": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "title": "Limit", + "description": "Maximum number of results to return", + "default": 50 + }, + "start_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Date", + "description": "Filter results to passages created after this datetime" + }, + "end_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "End Date", + "description": "Filter results to passages created before this datetime" + } + }, + "type": "object", + "title": "PassageSearchRequest", + "description": "Request model for searching passages across archives." + }, + "PassageSearchResult": { + "properties": { + "passage": { + "$ref": "#/components/schemas/Passage", + "description": "The passage object" + }, + "score": { + "type": "number", + "title": "Score", + "description": "Relevance score" + }, + "metadata": { + "additionalProperties": true, + "type": "object", + "title": "Metadata", + "description": "Additional metadata about the search result" + } + }, + "type": "object", + "required": ["passage", "score"], + "title": "PassageSearchResult", + "description": "Result from a passage search operation with scoring details." + }, + "PipRequirement": { + "properties": { + "name": { + "type": "string", + "minLength": 1, + "title": "Name", + "description": "Name of the pip package." + }, + "version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Version", + "description": "Optional version of the package, following semantic versioning." + } + }, + "type": "object", + "required": ["name"], + "title": "PipRequirement" + }, + "PromptTokensDetails": { + "properties": { + "audio_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Audio Tokens" + }, + "cached_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Cached Tokens" + } + }, + "additionalProperties": true, + "type": "object", + "title": "PromptTokensDetails", + "description": "Breakdown of tokens used in the prompt." + }, + "Provider": { + "properties": { + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Id", + "description": "The id of the provider, lazily created by the database manager." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the provider" + }, + "provider_type": { + "$ref": "#/components/schemas/ProviderType", + "description": "The type of the provider" + }, + "provider_category": { + "$ref": "#/components/schemas/ProviderCategory", + "description": "The category of the provider (base or byok)" + }, + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Api Key", + "description": "API key or secret key used for requests to the provider.", + "deprecated": true + }, + "base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Url", + "description": "Base URL for the provider." + }, + "access_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Access Key", + "description": "Access key used for requests to the provider.", + "deprecated": true + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Region", + "description": "Region used for requests to the provider." + }, + "api_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Api Version", + "description": "API version used for requests to the provider." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The last update timestamp of the provider." + }, + "last_synced": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Synced", + "description": "The last time models were synced for this provider." + }, + "api_key_enc": { + "anyOf": [ + { + "type": "string", + "description": "Encrypted secret value (stored as encrypted string)", + "nullable": true + }, + { + "type": "null" + } + ], + "title": "Api Key Enc", + "description": "Encrypted API key as Secret object" + }, + "access_key_enc": { + "anyOf": [ + { + "type": "string", + "description": "Encrypted secret value (stored as encrypted string)", + "nullable": true + }, + { + "type": "null" + } + ], + "title": "Access Key Enc", + "description": "Encrypted access key as Secret object" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["name", "provider_type", "provider_category"], + "title": "Provider" + }, + "ProviderCategory": { + "type": "string", + "enum": ["base", "byok"], + "title": "ProviderCategory" + }, + "ProviderCheck": { + "properties": { + "provider_type": { + "$ref": "#/components/schemas/ProviderType", + "description": "The type of the provider." + }, + "api_key": { + "type": "string", + "title": "Api Key", + "description": "API key or secret key used for requests to the provider." + }, + "access_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Access Key", + "description": "Access key used for requests to the provider." + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Region", + "description": "Region used for requests to the provider." + }, + "base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Url", + "description": "Base URL used for requests to the provider." + }, + "api_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Api Version", + "description": "API version used for requests to the provider." + } + }, + "type": "object", + "required": ["provider_type", "api_key"], + "title": "ProviderCheck" + }, + "ProviderCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the provider." + }, + "provider_type": { + "$ref": "#/components/schemas/ProviderType", + "description": "The type of the provider." + }, + "api_key": { + "type": "string", + "title": "Api Key", + "description": "API key or secret key used for requests to the provider." + }, + "access_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Access Key", + "description": "Access key used for requests to the provider." + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Region", + "description": "Region used for requests to the provider." + }, + "base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Url", + "description": "Base URL used for requests to the provider." + }, + "api_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Api Version", + "description": "API version used for requests to the provider." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["name", "provider_type", "api_key"], + "title": "ProviderCreate" + }, + "ProviderTrace": { + "properties": { + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this object." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this object." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The timestamp when the object was created." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the object was last updated." + }, + "id": { + "type": "string", + "pattern": "^provider_trace-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Provider_trace", + "examples": ["provider_trace-123e4567-e89b-12d3-a456-426614174000"] + }, + "request_json": { + "additionalProperties": true, + "type": "object", + "title": "Request Json", + "description": "JSON content of the provider request" + }, + "response_json": { + "additionalProperties": true, + "type": "object", + "title": "Response Json", + "description": "JSON content of the provider response" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id", + "description": "ID of the step that this trace is associated with" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "ID of the agent that generated this trace" + }, + "agent_tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Agent Tags", + "description": "Tags associated with the agent for filtering" + }, + "call_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Call Type", + "description": "Type of call (agent_step, summarization, etc.)" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id", + "description": "ID of the run this trace is associated with" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source", + "description": "Source service that generated this trace (memgpt-server, lettuce-py)" + }, + "org_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Org Id", + "description": "ID of the organization" + }, + "compaction_settings": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Compaction Settings", + "description": "Compaction/summarization settings (summarization calls only)" + }, + "llm_config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Llm Config", + "description": "LLM configuration used for this call (non-summarization calls only)" + }, + "billing_context": { + "anyOf": [ + { + "$ref": "#/components/schemas/BillingContext" + }, + { + "type": "null" + } + ], + "description": "Billing context from request headers" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["request_json", "response_json"], + "title": "ProviderTrace", + "description": "Letta's internal representation of a provider trace.\n\nAttributes:\n id (str): The unique identifier of the provider trace.\n request_json (Dict[str, Any]): JSON content of the provider request.\n response_json (Dict[str, Any]): JSON content of the provider response.\n step_id (str): ID of the step that this trace is associated with.\n agent_id (str): ID of the agent that generated this trace.\n agent_tags (list[str]): Tags associated with the agent for filtering.\n call_type (str): Type of call (agent_step, summarization, etc.).\n run_id (str): ID of the run this trace is associated with.\n source (str): Source service that generated this trace (memgpt-server, lettuce-py).\n organization_id (str): The unique identifier of the organization.\n user_id (str): The unique identifier of the user who initiated the request.\n compaction_settings (Dict[str, Any]): Compaction/summarization settings (only for summarization calls).\n llm_config (Dict[str, Any]): LLM configuration used for this call (only for non-summarization calls).\n created_at (datetime): The timestamp when the object was created." + }, + "ProviderType": { + "type": "string", + "enum": [ + "anthropic", + "azure", + "baseten", + "bedrock", + "cerebras", + "chatgpt_oauth", + "deepseek", + "fireworks", + "google_ai", + "google_vertex", + "groq", + "hugging-face", + "letta", + "lmstudio_openai", + "minimax", + "mistral", + "ollama", + "openai", + "together", + "vllm", + "sglang", + "openrouter", + "xai", + "zai", + "zai_coding" + ], + "title": "ProviderType" + }, + "ProviderUpdate": { + "properties": { + "api_key": { + "type": "string", + "title": "Api Key", + "description": "API key or secret key used for requests to the provider." + }, + "access_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Access Key", + "description": "Access key used for requests to the provider." + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Region", + "description": "Region used for requests to the provider." + }, + "base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Url", + "description": "Base URL used for requests to the provider." + }, + "api_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Api Version", + "description": "API version used for requests to the provider." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["api_key"], + "title": "ProviderUpdate" + }, + "ReasoningContent": { + "properties": { + "type": { + "type": "string", + "const": "reasoning", + "title": "Type", + "description": "Indicates this is a reasoning/intermediate step.", + "default": "reasoning" + }, + "is_native": { + "type": "boolean", + "title": "Is Native", + "description": "Whether the reasoning content was generated by a reasoner model that processed this step." + }, + "reasoning": { + "type": "string", + "title": "Reasoning", + "description": "The intermediate reasoning or thought process content." + }, + "signature": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Signature", + "description": "A unique identifier for this reasoning step." + } + }, + "type": "object", + "required": ["is_native", "reasoning"], + "title": "ReasoningContent", + "description": "Sent via the Anthropic Messages API" + }, + "ReasoningMessage": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "message_type": { + "type": "string", + "const": "reasoning_message", + "title": "Message Type", + "description": "The type of the message.", + "default": "reasoning_message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err" + }, + "seq_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seq Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + }, + "source": { + "type": "string", + "enum": ["reasoner_model", "non_reasoner_model"], + "title": "Source", + "default": "non_reasoner_model" + }, + "reasoning": { + "type": "string", + "title": "Reasoning" + }, + "signature": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Signature" + } + }, + "type": "object", + "required": ["id", "date", "reasoning"], + "title": "ReasoningMessage", + "description": "Representation of an agent's internal reasoning.\n\nArgs:\n id (str): The ID of the message\n date (datetime): The date the message was created in ISO format\n name (Optional[str]): The name of the sender of the message\n source (Literal[\"reasoner_model\", \"non_reasoner_model\"]): Whether the reasoning\n content was generated natively by a reasoner model or derived via prompting\n reasoning (str): The internal reasoning of the agent\n signature (Optional[str]): The model-generated signature of the reasoning step" + }, + "ReasoningMessageListResult": { + "properties": { + "reasoning": { + "type": "string", + "title": "Reasoning" + }, + "message_type": { + "type": "string", + "const": "reasoning_message", + "title": "Message Type", + "default": "reasoning_message" + }, + "message_id": { + "type": "string", + "title": "Message Id", + "description": "The unique identifier of the message." + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "The unique identifier of the agent that owns the message." + }, + "conversation_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Conversation Id", + "description": "The unique identifier of the conversation that the message belongs to." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The time the message was created in ISO format." + } + }, + "type": "object", + "required": ["reasoning", "message_id", "created_at"], + "title": "ReasoningMessageListResult", + "description": "Reasoning message list result with agent context.\n\nShape is identical to UpdateReasoningMessage but includes the owning agent_id and message id." + }, + "RedactedReasoningContent": { + "properties": { + "type": { + "type": "string", + "const": "redacted_reasoning", + "title": "Type", + "description": "Indicates this is a redacted thinking step.", + "default": "redacted_reasoning" + }, + "data": { + "type": "string", + "title": "Data", + "description": "The redacted or filtered intermediate reasoning content." + } + }, + "type": "object", + "required": ["data"], + "title": "RedactedReasoningContent", + "description": "Sent via the Anthropic Messages API" + }, + "RequiredBeforeExitToolRule": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name", + "description": "The name of the tool. Must exist in the database for the user's organization." + }, + "type": { + "type": "string", + "const": "required_before_exit", + "title": "Type", + "default": "required_before_exit" + }, + "prompt_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt Template", + "description": "Optional template string (ignored)." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["tool_name"], + "title": "RequiredBeforeExitToolRule", + "description": "Represents a tool rule configuration where this tool must be called before the agent loop can exit." + }, + "RequiresApprovalToolRule": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name", + "description": "The name of the tool. Must exist in the database for the user's organization." + }, + "type": { + "type": "string", + "const": "requires_approval", + "title": "Type", + "default": "requires_approval" + }, + "prompt_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt Template", + "description": "Optional template string (ignored). Rendering uses fast built-in formatting for performance." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["tool_name"], + "title": "RequiresApprovalToolRule", + "description": "Represents a tool rule configuration which requires approval before the tool can be invoked." + }, + "ResetMessagesRequest": { + "properties": { + "add_default_initial_messages": { + "type": "boolean", + "title": "Add Default Initial Messages", + "description": "If true, adds the default initial messages after resetting.", + "default": false + } + }, + "type": "object", + "title": "ResetMessagesRequest", + "description": "Request body for resetting messages on an agent." + }, + "RetrieveStreamRequest": { + "properties": { + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "Agent ID for agent-direct mode with 'default' conversation. Use with conversation_id='default' in the URL path." + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id", + "description": "Run ID to stream directly, bypassing run lookup. Use for recovery from duplicate requests." + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "Offline threading ID to look up the run_id. Bypasses active run lookup if run_id not provided." + }, + "starting_after": { + "type": "integer", + "title": "Starting After", + "description": "Sequence id to use as a cursor for pagination. Response will start streaming after this chunk sequence id", + "default": 0 + }, + "include_pings": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Include Pings", + "description": "Whether to include periodic keepalive ping messages in the stream to prevent connection timeouts.", + "default": true + }, + "poll_interval": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Poll Interval", + "description": "Seconds to wait between polls when no new data.", + "default": 0.1 + }, + "batch_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Batch Size", + "description": "Number of entries to read per batch.", + "default": 100 + } + }, + "type": "object", + "title": "RetrieveStreamRequest" + }, + "RoundRobinManager": { + "properties": { + "manager_type": { + "type": "string", + "const": "round_robin", + "title": "Manager Type", + "description": "", + "default": "round_robin" + }, + "max_turns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Turns", + "description": "" + } + }, + "type": "object", + "title": "RoundRobinManager" + }, + "RoundRobinManagerUpdate": { + "properties": { + "manager_type": { + "type": "string", + "const": "round_robin", + "title": "Manager Type", + "description": "", + "default": "round_robin" + }, + "max_turns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Turns", + "description": "" + } + }, + "type": "object", + "title": "RoundRobinManagerUpdate" + }, + "Run": { + "properties": { + "id": { + "type": "string", + "pattern": "^(job|run)-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Run", + "examples": ["run-123e4567-e89b-12d3-a456-426614174000"] + }, + "status": { + "$ref": "#/components/schemas/RunStatus", + "description": "The current status of the run.", + "default": "created" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The timestamp when the run was created." + }, + "completed_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Completed At", + "description": "The timestamp when the run was completed." + }, + "agent_id": { + "type": "string", + "title": "Agent Id", + "description": "The unique identifier of the agent associated with the run." + }, + "conversation_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Conversation Id", + "description": "The unique identifier of the conversation associated with the run." + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "The base template ID that the run belongs to." + }, + "background": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Background", + "description": "Whether the run was created in background mode." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Additional metadata for the run." + }, + "request_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/LettaRequestConfig" + }, + { + "type": "null" + } + ], + "description": "The request configuration for the run." + }, + "stop_reason": { + "anyOf": [ + { + "$ref": "#/components/schemas/StopReasonType" + }, + { + "type": "null" + } + ], + "description": "The reason why the run was stopped." + }, + "callback_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Callback Url", + "description": "If set, POST to this URL when the run completes." + }, + "callback_sent_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Callback Sent At", + "description": "Timestamp when the callback was last attempted." + }, + "callback_status_code": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Callback Status Code", + "description": "HTTP status code returned by the callback endpoint." + }, + "callback_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Callback Error", + "description": "Optional error message from attempting to POST the callback endpoint." + }, + "ttft_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Ttft Ns", + "description": "Time to first token for a run in nanoseconds" + }, + "total_duration_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Total Duration Ns", + "description": "Total run duration in nanoseconds" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["agent_id"], + "title": "Run", + "description": "Representation of a run - a conversation or processing session for an agent. Runs track when agents process messages and maintain the relationship between agents, steps, and messages." + }, + "RunMetrics": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of the run this metric belongs to (matches runs.id)." + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "The unique identifier of the agent." + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The project that the run belongs to (cloud only)." + }, + "run_start_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Run Start Ns", + "description": "The timestamp of the start of the run in nanoseconds." + }, + "run_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Run Ns", + "description": "Total time for the run in nanoseconds." + }, + "num_steps": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Num Steps", + "description": "The number of steps in the run." + }, + "tools_used": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tools Used", + "description": "List of tool IDs that were used in this run." + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The template ID that the run belongs to (cloud only)." + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "The base template ID that the run belongs to (cloud only)." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["id"], + "title": "RunMetrics" + }, + "RunStatus": { + "type": "string", + "enum": ["created", "running", "completed", "failed", "cancelled"], + "title": "RunStatus", + "description": "Status of the run." + }, + "SGLangModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "sglang", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "sglang" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "reasoning": { + "$ref": "#/components/schemas/OpenAIReasoning", + "description": "The reasoning configuration for the model.", + "default": { + "reasoning_effort": "high" + } + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model." + }, + "strict": { + "type": "boolean", + "title": "Strict", + "description": "Enable strict mode for tool calling. When true, tool outputs are guaranteed to match JSON schemas.", + "default": true + }, + "tool_call_parser": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tool Call Parser", + "description": "SGLang tool call parser name (for example 'glm47', 'qwen25', or 'hermes')." + } + }, + "type": "object", + "title": "SGLangModelSettings", + "description": "SGLang model configuration (OpenAI-compatible runtime with SGLang-specific parsing)." + }, + "SSEMCPServer": { + "properties": { + "mcp_server_type": { + "type": "string", + "const": "sse", + "title": "Mcp Server Type", + "default": "sse" + }, + "server_url": { + "type": "string", + "title": "Server Url", + "description": "The URL of the server" + }, + "auth_header": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Header", + "description": "The name of the authentication header (e.g., 'Authorization')" + }, + "auth_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Token", + "description": "The authentication token or API key value" + }, + "custom_headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Headers", + "description": "Custom HTTP headers to include with requests" + }, + "id": { + "type": "string", + "pattern": "^mcp_server-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Mcp_server", + "examples": ["mcp_server-123e4567-e89b-12d3-a456-426614174000"] + }, + "server_name": { + "type": "string", + "title": "Server Name", + "description": "The name of the MCP server" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["server_url", "server_name"], + "title": "SSEMCPServer", + "description": "An SSE MCP server" + }, + "SSEServerConfig": { + "properties": { + "server_name": { + "type": "string", + "title": "Server Name", + "description": "The name of the server" + }, + "type": { + "$ref": "#/components/schemas/MCPServerType", + "default": "sse" + }, + "server_url": { + "type": "string", + "title": "Server Url", + "description": "The URL of the server" + }, + "auth_header": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Header", + "description": "The name of the authentication header (e.g., 'Authorization')" + }, + "auth_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Token", + "description": "The authentication token or API key value" + }, + "custom_headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Headers", + "description": "Custom HTTP headers to include with requests" + } + }, + "type": "object", + "required": ["server_name", "server_url"], + "title": "SSEServerConfig", + "description": "Configuration for an MCP server using SSE" + }, + "SandboxConfig": { + "properties": { + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this object." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this object." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "The timestamp when the object was created." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the object was last updated." + }, + "id": { + "type": "string", + "pattern": "^sandbox-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Sandbox", + "examples": ["sandbox-123e4567-e89b-12d3-a456-426614174000"] + }, + "type": { + "$ref": "#/components/schemas/SandboxType", + "description": "The type of sandbox." + }, + "config": { + "additionalProperties": true, + "type": "object", + "title": "Config", + "description": "The JSON sandbox settings data." + } + }, + "additionalProperties": false, + "type": "object", + "title": "SandboxConfig" + }, + "SandboxConfigCreate": { + "properties": { + "config": { + "anyOf": [ + { + "$ref": "#/components/schemas/LocalSandboxConfig" + }, + { + "$ref": "#/components/schemas/E2BSandboxConfig" + }, + { + "$ref": "#/components/schemas/ModalSandboxConfig" + } + ], + "title": "Config", + "description": "The configuration for the sandbox." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["config"], + "title": "SandboxConfigCreate" + }, + "SandboxConfigUpdate": { + "properties": { + "config": { + "anyOf": [ + { + "$ref": "#/components/schemas/LocalSandboxConfig" + }, + { + "$ref": "#/components/schemas/E2BSandboxConfig" + }, + { + "$ref": "#/components/schemas/ModalSandboxConfig" + } + ], + "title": "Config", + "description": "The JSON configuration data for the sandbox." + } + }, + "additionalProperties": false, + "type": "object", + "title": "SandboxConfigUpdate", + "description": "Pydantic model for updating SandboxConfig fields." + }, + "SandboxEnvironmentVariable": { + "properties": { + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this object." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this object." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "The timestamp when the object was created." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the object was last updated." + }, + "id": { + "type": "string", + "pattern": "^sandbox-env-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Sandbox-env", + "examples": ["sandbox-env-123e4567-e89b-12d3-a456-426614174000"] + }, + "key": { + "type": "string", + "title": "Key", + "description": "The name of the environment variable." + }, + "value": { + "type": "string", + "title": "Value", + "description": "The value of the environment variable." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "An optional description of the environment variable." + }, + "value_enc": { + "anyOf": [ + { + "type": "string", + "description": "Encrypted secret value (stored as encrypted string)", + "nullable": true + }, + { + "type": "null" + } + ], + "title": "Value Enc", + "description": "Encrypted value as Secret object" + }, + "sandbox_config_id": { + "type": "string", + "title": "Sandbox Config Id", + "description": "The ID of the sandbox config this environment variable belongs to." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["key", "value", "sandbox_config_id"], + "title": "SandboxEnvironmentVariable" + }, + "SandboxEnvironmentVariableCreate": { + "properties": { + "key": { + "type": "string", + "title": "Key", + "description": "The name of the environment variable." + }, + "value": { + "type": "string", + "title": "Value", + "description": "The value of the environment variable." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "An optional description of the environment variable." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["key", "value"], + "title": "SandboxEnvironmentVariableCreate" + }, + "SandboxEnvironmentVariableUpdate": { + "properties": { + "key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Key", + "description": "The name of the environment variable." + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value", + "description": "The value of the environment variable." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "An optional description of the environment variable." + } + }, + "additionalProperties": false, + "type": "object", + "title": "SandboxEnvironmentVariableUpdate" + }, + "SandboxType": { + "type": "string", + "enum": ["e2b", "modal", "local"], + "title": "SandboxType" + }, + "SearchAllMessagesRequest": { + "properties": { + "query": { + "type": "string", + "title": "Query", + "description": "Text query for full-text search" + }, + "search_mode": { + "type": "string", + "enum": ["vector", "fts", "hybrid"], + "title": "Search Mode", + "description": "Search mode to use", + "default": "hybrid" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "Filter messages by agent ID" + }, + "conversation_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Conversation Id", + "description": "Filter messages by conversation ID" + }, + "limit": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "title": "Limit", + "description": "Maximum number of results to return", + "default": 50 + }, + "start_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Date", + "description": "Filter messages created after this date" + }, + "end_date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "End Date", + "description": "Filter messages created on or before this date" + } + }, + "type": "object", + "required": ["query"], + "title": "SearchAllMessagesRequest" + }, + "SearchCacheWarmRequest": { + "properties": { + "collection": { + "type": "string", + "const": "messages", + "title": "Collection", + "description": "Embedded collection whose cache should be warmed." + }, + "scope": { + "$ref": "#/components/schemas/MessageSearchCacheWarmScope", + "description": "Collection-specific scope. Messages currently infer organization from the authenticated actor." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["collection", "scope"], + "title": "SearchCacheWarmRequest", + "description": "Request for warming an internal search cache." + }, + "SearchCacheWarmResponse": { + "properties": { + "collection": { + "type": "string", + "const": "messages", + "title": "Collection" + }, + "status": { + "type": "string", + "title": "Status" + }, + "warmed": { + "type": "boolean", + "title": "Warmed" + } + }, + "type": "object", + "required": ["collection", "status", "warmed"], + "title": "SearchCacheWarmResponse", + "description": "Response for internal search cache warming." + }, + "SkillSchema": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Skill name, also serves as unique identifier (e.g., 'slack', 'pdf')" + }, + "files": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Files", + "description": "Skill files as path -> content mapping. Must include 'SKILL.md' key if provided." + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Url", + "description": "Source URL for skill resolution (e.g., 'letta:slack', 'anthropic:pdf', 'owner/repo/path')" + } + }, + "type": "object", + "required": ["name"], + "title": "SkillSchema", + "description": "Skill schema for agent files.\n\nSkills are folders of instructions, scripts, and resources that agents can load.\nEither files (with SKILL.md) or source_url must be provided:\n- files with SKILL.md: inline skill content\n- source_url: reference to resolve later (e.g., 'letta:slack')\n- both: inline content with provenance tracking" + }, + "SleeptimeManager": { + "properties": { + "manager_type": { + "type": "string", + "const": "sleeptime", + "title": "Manager Type", + "description": "", + "default": "sleeptime" + }, + "manager_agent_id": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "title": "Manager Agent Id", + "description": "", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + "sleeptime_agent_frequency": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Sleeptime Agent Frequency", + "description": "" + } + }, + "type": "object", + "required": ["manager_agent_id"], + "title": "SleeptimeManager" + }, + "SleeptimeManagerSchema": { + "properties": { + "manager_type": { + "type": "string", + "const": "sleeptime", + "title": "Manager Type", + "description": "", + "default": "sleeptime" + }, + "manager_agent_id": { + "type": "string", + "title": "Manager Agent Id", + "description": "" + }, + "sleeptime_agent_frequency": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Sleeptime Agent Frequency", + "description": "" + } + }, + "type": "object", + "required": ["manager_agent_id"], + "title": "SleeptimeManagerSchema" + }, + "SleeptimeManagerUpdate": { + "properties": { + "manager_type": { + "type": "string", + "const": "sleeptime", + "title": "Manager Type", + "description": "", + "default": "sleeptime" + }, + "manager_agent_id": { + "anyOf": [ + { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + { + "type": "null" + } + ], + "title": "Manager Agent Id", + "description": "" + }, + "sleeptime_agent_frequency": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Sleeptime Agent Frequency", + "description": "" + } + }, + "type": "object", + "title": "SleeptimeManagerUpdate" + }, + "Source": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the source." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the source." + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Instructions", + "description": "Instructions for how to use the source." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata associated with the source." + }, + "id": { + "type": "string", + "pattern": "^source-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Source", + "examples": ["source-123e4567-e89b-12d3-a456-426614174000"] + }, + "embedding_config": { + "$ref": "#/components/schemas/EmbeddingConfig", + "description": "The embedding configuration used by the source." + }, + "vector_db_provider": { + "$ref": "#/components/schemas/VectorDBProvider", + "description": "The vector database provider used for this source's passages", + "default": "native" + }, + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this Tool." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this Tool." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "The timestamp when the source was created." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The timestamp when the source was last updated." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["name", "embedding_config"], + "title": "Source", + "description": "(Deprecated: Use Folder) Representation of a source, which is a collection of files and passages." + }, + "SourceCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the source." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the source." + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Instructions", + "description": "Instructions for how to use the source." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata associated with the source." + }, + "embedding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Embedding", + "description": "The handle for the embedding config used by the source." + }, + "embedding_chunk_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Embedding Chunk Size", + "description": "The chunk size of the embedding." + }, + "embedding_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + { + "type": "null" + } + ], + "description": "(Legacy) The embedding configuration used by the source." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["name"], + "title": "SourceCreate", + "description": "Schema for creating a new Source." + }, + "SourceSchema": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the source." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the source." + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Instructions", + "description": "Instructions for how to use the source." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata associated with the source." + }, + "embedding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Embedding", + "description": "The handle for the embedding config used by the source." + }, + "embedding_chunk_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Embedding Chunk Size", + "description": "The chunk size of the embedding." + }, + "embedding_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + { + "type": "null" + } + ], + "description": "(Legacy) The embedding configuration used by the source." + }, + "id": { + "type": "string", + "title": "Id", + "description": "Human-readable identifier for this source in the file" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["name", "id"], + "title": "SourceSchema", + "description": "Source with human-readable ID for agent file" + }, + "SourceStats": { + "properties": { + "source_id": { + "type": "string", + "title": "Source Id", + "description": "Deprecated: Use `folder_id` field instead. Unique identifier of the source", + "deprecated": true + }, + "source_name": { + "type": "string", + "title": "Source Name", + "description": "Deprecated: Use `folder_name` field instead. Name of the source", + "deprecated": true + }, + "file_count": { + "type": "integer", + "title": "File Count", + "description": "Number of files in the source", + "default": 0 + }, + "total_size": { + "type": "integer", + "title": "Total Size", + "description": "Total size of all files in bytes", + "default": 0 + }, + "files": { + "items": { + "$ref": "#/components/schemas/FileStats" + }, + "type": "array", + "title": "Files", + "description": "List of file statistics" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["source_id", "source_name"], + "title": "SourceStats", + "description": "Aggregated metadata for a source" + }, + "SourceUpdate": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "The name of the source." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the source." + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Instructions", + "description": "Instructions for how to use the source." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "Metadata associated with the source." + }, + "embedding_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + { + "type": "null" + } + ], + "description": "The embedding configuration used by the source." + } + }, + "additionalProperties": false, + "type": "object", + "title": "SourceUpdate", + "description": "Schema for updating an existing Source." + }, + "StdioMCPServer": { + "properties": { + "mcp_server_type": { + "type": "string", + "const": "stdio", + "title": "Mcp Server Type", + "default": "stdio" + }, + "command": { + "type": "string", + "title": "Command", + "description": "The command to run (MCP 'local' client will run this command)" + }, + "args": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Args", + "description": "The arguments to pass to the command" + }, + "env": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Env", + "description": "Environment variables to set" + }, + "id": { + "type": "string", + "pattern": "^mcp_server-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Mcp_server", + "examples": ["mcp_server-123e4567-e89b-12d3-a456-426614174000"] + }, + "server_name": { + "type": "string", + "title": "Server Name", + "description": "The name of the MCP server" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["command", "args", "server_name"], + "title": "StdioMCPServer", + "description": "A Stdio MCP server" + }, + "StdioServerConfig": { + "properties": { + "server_name": { + "type": "string", + "title": "Server Name", + "description": "The name of the server" + }, + "type": { + "$ref": "#/components/schemas/MCPServerType", + "default": "stdio" + }, + "command": { + "type": "string", + "title": "Command", + "description": "The command to run (MCP 'local' client will run this command)" + }, + "args": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Args", + "description": "The arguments to pass to the command" + }, + "env": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Env", + "description": "Environment variables to set" + } + }, + "type": "object", + "required": ["server_name", "command", "args"], + "title": "StdioServerConfig" + }, + "Step": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of the step. Assigned by the database." + }, + "origin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Origin", + "description": "The surface that this agent step was initiated from." + }, + "provider_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider Id", + "description": "The unique identifier of the provider that was configured for this step" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id", + "description": "The unique identifier of the run that this step belongs to. Only included for async calls." + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "The ID of the agent that performed the step." + }, + "provider_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider Name", + "description": "The name of the provider used for this step." + }, + "provider_category": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider Category", + "description": "The category of the provider used for this step." + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "The name of the model used for this step." + }, + "model_handle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model Handle", + "description": "The model handle (e.g., 'openai/gpt-4o-mini') used for this step." + }, + "model_endpoint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model Endpoint", + "description": "The model endpoint url used for this step." + }, + "context_window_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Context Window Limit", + "description": "The context window limit configured for this step." + }, + "completion_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Completion Tokens", + "description": "The number of tokens generated by the agent during this step." + }, + "prompt_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Prompt Tokens", + "description": "The number of tokens in the prompt during this step." + }, + "total_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Total Tokens", + "description": "The total number of tokens processed by the agent during this step." + }, + "cached_input_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Cached Input Tokens", + "description": "The number of input tokens served from cache. None if not reported by provider." + }, + "cache_write_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Cache Write Tokens", + "description": "The number of input tokens written to cache (Anthropic only). None if not reported by provider." + }, + "reasoning_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Reasoning Tokens", + "description": "The number of reasoning/thinking tokens generated. None if not reported by provider." + }, + "completion_tokens_details": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Completion Tokens Details", + "description": "Detailed completion token breakdown (e.g., reasoning_tokens)." + }, + "prompt_tokens_details": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Prompt Tokens Details", + "description": "Detailed prompt token breakdown (e.g., cached_tokens, cache_read_tokens, cache_creation_tokens)." + }, + "stop_reason": { + "anyOf": [ + { + "$ref": "#/components/schemas/StopReasonType" + }, + { + "type": "null" + } + ], + "description": "The stop reason associated with the step." + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags", + "description": "Metadata tags.", + "default": [] + }, + "tid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tid", + "description": "The unique identifier of the transaction that processed this step." + }, + "trace_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Trace Id", + "description": "The trace id of the agent step." + }, + "request_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Request Id", + "description": "The API request log ID from cloud-api for correlating steps with API requests." + }, + "messages": { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array", + "title": "Messages", + "description": "The messages generated during this step. Deprecated: use `GET /v1/steps/{step_id}/messages` endpoint instead", + "default": [], + "deprecated": true + }, + "feedback": { + "anyOf": [ + { + "type": "string", + "enum": ["positive", "negative"] + }, + { + "type": "null" + } + ], + "title": "Feedback", + "description": "The feedback for this step. Must be either 'positive' or 'negative'." + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The project that the agent that executed this step belongs to (cloud only)." + }, + "error_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error Type", + "description": "The type/class of the error that occurred" + }, + "error_data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Error Data", + "description": "Error details including message, traceback, and additional context" + }, + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/StepStatus" + }, + { + "type": "null" + } + ], + "description": "Step status: pending, success, or failed", + "default": "pending" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["id"], + "title": "Step" + }, + "StepMetrics": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "The id of the step this metric belongs to (matches steps.id)." + }, + "provider_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider Id", + "description": "The unique identifier of the provider." + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id", + "description": "The unique identifier of the run." + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "The unique identifier of the agent." + }, + "step_start_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Step Start Ns", + "description": "The timestamp of the start of the step in nanoseconds." + }, + "llm_request_start_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Llm Request Start Ns", + "description": "The timestamp of the start of the llm request in nanoseconds." + }, + "llm_request_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Llm Request Ns", + "description": "Time spent on LLM requests in nanoseconds." + }, + "tool_execution_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Tool Execution Ns", + "description": "Time spent on tool execution in nanoseconds." + }, + "step_ns": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Step Ns", + "description": "Total time for the step in nanoseconds." + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "The base template ID that the step belongs to (cloud only)." + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The template ID that the step belongs to (cloud only)." + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The project that the step belongs to (cloud only)." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["id"], + "title": "StepMetrics" + }, + "StepStatus": { + "type": "string", + "enum": ["pending", "success", "failed", "cancelled"], + "title": "StepStatus", + "description": "Status of a step execution" + }, + "StopReasonType": { + "type": "string", + "enum": [ + "end_turn", + "error", + "llm_api_error", + "invalid_llm_response", + "invalid_tool_call", + "max_steps", + "max_tokens_exceeded", + "no_tool_call", + "tool_rule", + "cancelled", + "insufficient_credits", + "requires_approval", + "context_window_overflow_in_system_prompt" + ], + "title": "StopReasonType" + }, + "StreamableHTTPMCPServer": { + "properties": { + "mcp_server_type": { + "type": "string", + "const": "streamable_http", + "title": "Mcp Server Type", + "default": "streamable_http" + }, + "server_url": { + "type": "string", + "title": "Server Url", + "description": "The URL of the server" + }, + "auth_header": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Header", + "description": "The name of the authentication header (e.g., 'Authorization')" + }, + "auth_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Token", + "description": "The authentication token or API key value" + }, + "custom_headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Headers", + "description": "Custom HTTP headers to include with requests" + }, + "id": { + "type": "string", + "pattern": "^mcp_server-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Mcp_server", + "examples": ["mcp_server-123e4567-e89b-12d3-a456-426614174000"] + }, + "server_name": { + "type": "string", + "title": "Server Name", + "description": "The name of the MCP server" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["server_url", "server_name"], + "title": "StreamableHTTPMCPServer", + "description": "A Streamable HTTP MCP server" + }, + "StreamableHTTPServerConfig": { + "properties": { + "server_name": { + "type": "string", + "title": "Server Name", + "description": "The name of the server" + }, + "type": { + "$ref": "#/components/schemas/MCPServerType", + "default": "streamable_http" + }, + "server_url": { + "type": "string", + "title": "Server Url", + "description": "The URL of the server" + }, + "auth_header": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Header", + "description": "The name of the authentication header (e.g., 'Authorization')" + }, + "auth_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Token", + "description": "The authentication token or API key value" + }, + "custom_headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Headers", + "description": "Custom HTTP headers to include with requests" + } + }, + "type": "object", + "required": ["server_name", "server_url"], + "title": "StreamableHTTPServerConfig", + "description": "Configuration for an MCP server using Streamable HTTP" + }, + "SummarizedReasoningContent": { + "properties": { + "type": { + "type": "string", + "const": "summarized_reasoning", + "title": "Type", + "description": "Indicates this is a summarized reasoning step.", + "default": "summarized_reasoning" + }, + "id": { + "type": "string", + "title": "Id", + "description": "The unique identifier for this reasoning step." + }, + "summary": { + "items": { + "$ref": "#/components/schemas/SummarizedReasoningContentPart" + }, + "type": "array", + "title": "Summary", + "description": "Summaries of the reasoning content." + }, + "encrypted_content": { + "type": "string", + "title": "Encrypted Content", + "description": "The encrypted reasoning content." + } + }, + "type": "object", + "required": ["id", "summary"], + "title": "SummarizedReasoningContent", + "description": "The style of reasoning content returned by the OpenAI Responses API" + }, + "SummarizedReasoningContentPart": { + "properties": { + "index": { + "type": "integer", + "title": "Index", + "description": "The index of the summary part." + }, + "text": { + "type": "string", + "title": "Text", + "description": "The text of the summary part." + } + }, + "type": "object", + "required": ["index", "text"], + "title": "SummarizedReasoningContentPart" + }, + "SummaryMessage": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "message_type": { + "type": "string", + "const": "summary_message", + "title": "Message Type", + "default": "summary_message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err" + }, + "seq_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seq Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + }, + "summary": { + "type": "string", + "title": "Summary" + }, + "compaction_stats": { + "anyOf": [ + { + "$ref": "#/components/schemas/CompactionStats" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "required": ["id", "date", "summary"], + "title": "SummaryMessage", + "description": "A message representing a summary of the conversation. Sent to the LLM as a user or system message depending on the provider." + }, + "SupervisorManager": { + "properties": { + "manager_type": { + "type": "string", + "const": "supervisor", + "title": "Manager Type", + "description": "", + "default": "supervisor" + }, + "manager_agent_id": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "title": "Manager Agent Id", + "description": "", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + } + }, + "type": "object", + "required": ["manager_agent_id"], + "title": "SupervisorManager" + }, + "SupervisorManagerSchema": { + "properties": { + "manager_type": { + "type": "string", + "const": "supervisor", + "title": "Manager Type", + "description": "", + "default": "supervisor" + }, + "manager_agent_id": { + "type": "string", + "title": "Manager Agent Id", + "description": "" + } + }, + "type": "object", + "required": ["manager_agent_id"], + "title": "SupervisorManagerSchema" + }, + "SupervisorManagerUpdate": { + "properties": { + "manager_type": { + "type": "string", + "const": "supervisor", + "title": "Manager Type", + "description": "", + "default": "supervisor" + }, + "manager_agent_id": { + "anyOf": [ + { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + { + "type": "null" + } + ], + "title": "Manager Agent Id", + "description": "" + } + }, + "type": "object", + "required": ["manager_agent_id"], + "title": "SupervisorManagerUpdate" + }, + "SystemMessage": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "message_type": { + "type": "string", + "const": "system_message", + "title": "Message Type", + "description": "The type of the message.", + "default": "system_message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err" + }, + "seq_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seq Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + }, + "content": { + "type": "string", + "title": "Content", + "description": "The message content sent by the system" + } + }, + "type": "object", + "required": ["id", "date", "content"], + "title": "SystemMessage", + "description": "A message generated by the system. Never streamed back on a response, only used for cursor pagination.\n\nArgs:\n id (str): The ID of the message\n date (datetime): The date the message was created in ISO format\n name (Optional[str]): The name of the sender of the message\n content (str): The message content sent by the system" + }, + "SystemMessageListResult": { + "properties": { + "message_type": { + "type": "string", + "const": "system_message", + "title": "Message Type", + "default": "system_message" + }, + "content": { + "type": "string", + "title": "Content", + "description": "The message content sent by the system (can be a string or an array of multi-modal content parts)" + }, + "message_id": { + "type": "string", + "title": "Message Id", + "description": "The unique identifier of the message." + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "The unique identifier of the agent that owns the message." + }, + "conversation_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Conversation Id", + "description": "The unique identifier of the conversation that the message belongs to." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The time the message was created in ISO format." + } + }, + "type": "object", + "required": ["content", "message_id", "created_at"], + "title": "SystemMessageListResult", + "description": "System message list result with agent context.\n\nShape is identical to UpdateSystemMessage but includes the owning agent_id and message id." + }, + "TagSchema": { + "properties": { + "tag": { + "type": "string", + "title": "Tag" + } + }, + "type": "object", + "required": ["tag"], + "title": "TagSchema" + }, + "TerminalToolRule": { + "properties": { + "tool_name": { + "type": "string", + "title": "Tool Name", + "description": "The name of the tool. Must exist in the database for the user's organization." + }, + "type": { + "type": "string", + "const": "exit_loop", + "title": "Type", + "default": "exit_loop" + }, + "prompt_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt Template", + "description": "Optional template string (ignored)." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["tool_name"], + "title": "TerminalToolRule", + "description": "Represents a terminal tool rule configuration where if this tool gets called, it must end the agent loop." + }, + "TextContent": { + "properties": { + "type": { + "type": "string", + "const": "text", + "title": "Type", + "description": "The type of the message.", + "default": "text" + }, + "text": { + "type": "string", + "title": "Text", + "description": "The text content of the message." + }, + "signature": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Signature", + "description": "Stores a unique identifier for any reasoning associated with this text content." + } + }, + "type": "object", + "required": ["text"], + "title": "TextContent" + }, + "TextResponseFormat": { + "properties": { + "type": { + "type": "string", + "const": "text", + "title": "Type", + "description": "The type of the response format.", + "default": "text" + } + }, + "type": "object", + "title": "TextResponseFormat", + "description": "Response format for plain text responses." + }, + "TogetherModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "together", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "together" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model." + } + }, + "type": "object", + "title": "TogetherModelSettings", + "description": "Together AI model configuration (OpenAI-compatible)." + }, + "Tool": { + "properties": { + "id": { + "type": "string", + "pattern": "^tool-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the Tool", + "examples": ["tool-123e4567-e89b-12d3-a456-426614174000"] + }, + "tool_type": { + "$ref": "#/components/schemas/ToolType", + "description": "The type of the tool.", + "default": "custom" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the tool." + }, + "source_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Type", + "description": "The type of the source code." + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "The name of the function." + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags", + "description": "Metadata tags.", + "default": [] + }, + "source_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Code", + "description": "The source code of the function." + }, + "json_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Json Schema", + "description": "The JSON schema of the function." + }, + "args_json_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Args Json Schema", + "description": "The args JSON schema of the function." + }, + "return_char_limit": { + "type": "integer", + "maximum": 1000000, + "minimum": 1, + "title": "Return Char Limit", + "description": "The maximum number of characters in the response.", + "default": 50000 + }, + "pip_requirements": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/PipRequirement" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Pip Requirements", + "description": "Optional list of pip packages required by this tool." + }, + "npm_requirements": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/NpmRequirement" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Npm Requirements", + "description": "Optional list of npm packages required by this tool." + }, + "default_requires_approval": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Default Requires Approval", + "description": "Default value for whether or not executing this tool requires approval." + }, + "enable_parallel_execution": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Parallel Execution", + "description": "If set to True, then this tool will potentially be executed concurrently with other tools. Default False.", + "default": false + }, + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this Tool." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this Tool." + }, + "metadata_": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "A dictionary of additional metadata for the tool." + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The project id of the tool." + } + }, + "additionalProperties": false, + "type": "object", + "title": "Tool", + "description": "Representation of a tool, which is a function that can be called by the agent." + }, + "ToolAnnotations": { + "properties": { + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" + }, + "readOnlyHint": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Readonlyhint" + }, + "destructiveHint": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Destructivehint" + }, + "idempotentHint": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Idempotenthint" + }, + "openWorldHint": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Openworldhint" + } + }, + "additionalProperties": true, + "type": "object", + "title": "ToolAnnotations", + "description": "Additional properties describing a Tool to clients.\n\nNOTE: all properties in ToolAnnotations are **hints**.\nThey are not guaranteed to provide a faithful description of\ntool behavior (including descriptive properties like `title`).\n\nClients should never make tool use decisions based on ToolAnnotations\nreceived from untrusted servers." + }, + "ToolCall": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "arguments": { + "type": "string", + "title": "Arguments" + }, + "tool_call_id": { + "type": "string", + "title": "Tool Call Id" + } + }, + "type": "object", + "required": ["name", "arguments", "tool_call_id"], + "title": "ToolCall" + }, + "ToolCallContent": { + "properties": { + "type": { + "type": "string", + "const": "tool_call", + "title": "Type", + "description": "Indicates this content represents a tool call event.", + "default": "tool_call" + }, + "id": { + "type": "string", + "title": "Id", + "description": "A unique identifier for this specific tool call instance." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the tool being called." + }, + "input": { + "additionalProperties": true, + "type": "object", + "title": "Input", + "description": "The parameters being passed to the tool, structured as a dictionary of parameter names to values." + }, + "signature": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Signature", + "description": "Stores a unique identifier for any reasoning associated with this tool call." + } + }, + "type": "object", + "required": ["id", "name", "input"], + "title": "ToolCallContent" + }, + "ToolCallDelta": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "arguments": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Arguments" + }, + "tool_call_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tool Call Id" + } + }, + "type": "object", + "title": "ToolCallDelta" + }, + "ToolCallMessage": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "message_type": { + "type": "string", + "const": "tool_call_message", + "title": "Message Type", + "description": "The type of the message.", + "default": "tool_call_message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err" + }, + "seq_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seq Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + }, + "tool_call": { + "anyOf": [ + { + "$ref": "#/components/schemas/ToolCall" + }, + { + "$ref": "#/components/schemas/ToolCallDelta" + } + ], + "title": "Tool Call", + "deprecated": true + }, + "tool_calls": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ToolCall" + }, + "type": "array" + }, + { + "$ref": "#/components/schemas/ToolCallDelta" + }, + { + "type": "null" + } + ], + "title": "Tool Calls" + } + }, + "type": "object", + "required": ["id", "date", "tool_call"], + "title": "ToolCallMessage", + "description": "A message representing a request to call a tool (generated by the LLM to trigger tool execution).\n\nArgs:\n id (str): The ID of the message\n date (datetime): The date the message was created in ISO format\n name (Optional[str]): The name of the sender of the message\n tool_call (Union[ToolCall, ToolCallDelta]): The tool call" + }, + "ToolCallNode": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the child tool to invoke next." + }, + "args": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Args", + "description": "Optional prefilled arguments for this child tool. Keys must match the tool's parameter names and values must satisfy the tool's JSON schema. Supports partial prefill; non-overlapping parameters are left to the model." + } + }, + "type": "object", + "required": ["name"], + "title": "ToolCallNode", + "description": "Typed child override for prefilled arguments.\n\nWhen used in a ChildToolRule, if this child is selected next, its `args` will be\napplied as prefilled arguments (overriding overlapping LLM-provided values)." + }, + "ToolCreate": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the tool." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "Metadata tags." + }, + "source_code": { + "type": "string", + "title": "Source Code", + "description": "The source code of the function." + }, + "source_type": { + "type": "string", + "title": "Source Type", + "description": "The source type of the function.", + "default": "python" + }, + "json_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Json Schema", + "description": "The JSON schema of the function (auto-generated from source_code if not provided)" + }, + "args_json_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Args Json Schema", + "description": "The args JSON schema of the function." + }, + "return_char_limit": { + "type": "integer", + "maximum": 1000000, + "minimum": 1, + "title": "Return Char Limit", + "description": "The maximum number of characters in the response.", + "default": 50000 + }, + "pip_requirements": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/PipRequirement" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Pip Requirements", + "description": "Optional list of pip packages required by this tool." + }, + "npm_requirements": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/NpmRequirement" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Npm Requirements", + "description": "Optional list of npm packages required by this tool." + }, + "default_requires_approval": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Default Requires Approval", + "description": "Whether or not to require approval before executing this tool." + }, + "enable_parallel_execution": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Parallel Execution", + "description": "If set to True, then this tool will potentially be executed concurrently with other tools. Default False.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "required": ["source_code"], + "title": "ToolCreate" + }, + "ToolEnvVarSchema": { + "properties": { + "created_at": { + "type": "string", + "title": "Created At" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "key": { + "type": "string", + "title": "Key" + }, + "updated_at": { + "type": "string", + "title": "Updated At" + }, + "value": { + "type": "string", + "title": "Value" + } + }, + "type": "object", + "required": ["created_at", "description", "key", "updated_at", "value"], + "title": "ToolEnvVarSchema" + }, + "ToolExecutionResult": { + "properties": { + "status": { + "type": "string", + "enum": ["success", "error"], + "title": "Status", + "description": "The status of the tool execution and return object" + }, + "func_return": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "title": "Func Return", + "description": "The function return object" + }, + "agent_state": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentState" + }, + { + "type": "null" + } + ], + "description": "The agent state", + "deprecated": true + }, + "stdout": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Stdout", + "description": "Captured stdout (prints, logs) from function invocation" + }, + "stderr": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Stderr", + "description": "Captured stderr from the function invocation" + }, + "sandbox_config_fingerprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sandbox Config Fingerprint", + "description": "The fingerprint of the config for the sandbox" + } + }, + "type": "object", + "required": ["status"], + "title": "ToolExecutionResult" + }, + "ToolJSONSchema": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "type": "string", + "title": "Description" + }, + "parameters": { + "$ref": "#/components/schemas/ParametersSchema" + }, + "type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Type" + }, + "required": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Required" + } + }, + "type": "object", + "required": ["name", "description", "parameters"], + "title": "ToolJSONSchema" + }, + "ToolReturnContent": { + "properties": { + "type": { + "type": "string", + "const": "tool_return", + "title": "Type", + "description": "Indicates this content represents a tool return event.", + "default": "tool_return" + }, + "tool_call_id": { + "type": "string", + "title": "Tool Call Id", + "description": "References the ID of the ToolCallContent that initiated this tool call." + }, + "content": { + "type": "string", + "title": "Content", + "description": "The content returned by the tool execution." + }, + "is_error": { + "type": "boolean", + "title": "Is Error", + "description": "Indicates whether the tool execution resulted in an error." + } + }, + "type": "object", + "required": ["tool_call_id", "content", "is_error"], + "title": "ToolReturnContent" + }, + "ToolReturnCreate": { + "properties": { + "type": { + "type": "string", + "const": "tool_return", + "title": "Type", + "description": "The message type to be created.", + "default": "tool_return" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "group_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Group Id", + "description": "The multi-agent group that the message was sent in" + }, + "tool_returns": { + "items": { + "$ref": "#/components/schemas/letta__schemas__letta_message__ToolReturn" + }, + "type": "array", + "title": "Tool Returns", + "description": "List of tool returns from client-side execution" + } + }, + "type": "object", + "required": ["tool_returns"], + "title": "ToolReturnCreate", + "description": "Submit tool return(s) from client-side tool execution.\n\nThis is the preferred way to send tool results back to the agent after\nclient-side tool execution. It is equivalent to sending an ApprovalCreate\nwith tool return approvals, but provides a cleaner API for the common case." + }, + "ToolReturnMessage": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "message_type": { + "type": "string", + "const": "tool_return_message", + "title": "Message Type", + "description": "The type of the message.", + "default": "tool_return_message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err" + }, + "seq_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seq Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + }, + "tool_return": { + "type": "string", + "title": "Tool Return", + "deprecated": true + }, + "status": { + "type": "string", + "enum": ["success", "error"], + "title": "Status", + "deprecated": true + }, + "tool_call_id": { + "type": "string", + "title": "Tool Call Id", + "deprecated": true + }, + "stdout": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Stdout", + "deprecated": true + }, + "stderr": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Stderr", + "deprecated": true + }, + "tool_returns": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/letta__schemas__letta_message__ToolReturn" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Returns" + } + }, + "type": "object", + "required": ["id", "date", "tool_return", "status", "tool_call_id"], + "title": "ToolReturnMessage", + "description": "A message representing the return value of a tool call (generated by Letta executing the requested tool).\n\nArgs:\n id (str): The ID of the message\n date (datetime): The date the message was created in ISO format\n name (Optional[str]): The name of the sender of the message\n tool_return (str): The return value of the tool (deprecated, use tool_returns)\n status (Literal[\"success\", \"error\"]): The status of the tool call (deprecated, use tool_returns)\n tool_call_id (str): A unique identifier for the tool call that generated this message (deprecated, use tool_returns)\n stdout (Optional[List(str)]): Captured stdout (e.g. prints, logs) from the tool invocation (deprecated, use tool_returns)\n stderr (Optional[List(str)]): Captured stderr from the tool invocation (deprecated, use tool_returns)\n tool_returns (Optional[List[ToolReturn]]): List of tool returns for multi-tool support" + }, + "ToolRunFromSource": { + "properties": { + "source_code": { + "type": "string", + "title": "Source Code", + "description": "The source code of the function." + }, + "args": { + "additionalProperties": true, + "type": "object", + "title": "Args", + "description": "The arguments to pass to the tool." + }, + "env_vars": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Env Vars", + "description": "The environment variables to pass to the tool." + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "The name of the tool to run." + }, + "source_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Type", + "description": "The type of the source code." + }, + "args_json_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Args Json Schema", + "description": "The args JSON schema of the function." + }, + "json_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Json Schema", + "description": "The JSON schema of the function (auto-generated from source_code if not provided)" + }, + "pip_requirements": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/PipRequirement" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Pip Requirements", + "description": "Optional list of pip packages required by this tool." + }, + "npm_requirements": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/NpmRequirement" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Npm Requirements", + "description": "Optional list of npm packages required by this tool." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["source_code", "args"], + "title": "ToolRunFromSource" + }, + "ToolSearchRequest": { + "properties": { + "query": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Query", + "description": "Text query for semantic search." + }, + "search_mode": { + "type": "string", + "enum": ["vector", "fts", "hybrid"], + "title": "Search Mode", + "description": "Search mode: vector, fts, or hybrid.", + "default": "hybrid" + }, + "tool_types": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Types", + "description": "Filter by tool types (e.g., 'custom', 'letta_core')." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "Filter by tags (match any)." + }, + "limit": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "title": "Limit", + "description": "Maximum number of results to return.", + "default": 50 + } + }, + "additionalProperties": false, + "type": "object", + "title": "ToolSearchRequest", + "description": "Request model for searching tools using semantic search." + }, + "ToolSearchResult": { + "properties": { + "tool": { + "$ref": "#/components/schemas/Tool", + "description": "The matched tool." + }, + "embedded_text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Embedded Text", + "description": "The embedded text content used for matching." + }, + "fts_rank": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Fts Rank", + "description": "Full-text search rank position." + }, + "vector_rank": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Vector Rank", + "description": "Vector search rank position." + }, + "combined_score": { + "type": "number", + "title": "Combined Score", + "description": "Combined relevance score (RRF for hybrid mode)." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["tool", "combined_score"], + "title": "ToolSearchResult", + "description": "Result from a tool search operation." + }, + "ToolType": { + "type": "string", + "enum": [ + "custom", + "letta_core", + "letta_memory_core", + "letta_multi_agent_core", + "letta_sleeptime_core", + "letta_voice_sleeptime_core", + "letta_builtin", + "letta_files_core", + "external_langchain", + "external_composio", + "external_mcp" + ], + "title": "ToolType" + }, + "ToolUpdate": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the tool." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "Metadata tags." + }, + "source_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Code", + "description": "The source code of the function." + }, + "source_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Type", + "description": "The type of the source code." + }, + "json_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Json Schema", + "description": "The JSON schema of the function (auto-generated from source_code if not provided)" + }, + "args_json_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Args Json Schema", + "description": "The args JSON schema of the function." + }, + "return_char_limit": { + "anyOf": [ + { + "type": "integer", + "maximum": 1000000, + "minimum": 1 + }, + { + "type": "null" + } + ], + "title": "Return Char Limit", + "description": "The maximum number of characters in the response." + }, + "pip_requirements": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/PipRequirement" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Pip Requirements", + "description": "Optional list of pip packages required by this tool." + }, + "npm_requirements": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/NpmRequirement" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Npm Requirements", + "description": "Optional list of npm packages required by this tool." + }, + "metadata_": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "A dictionary of additional metadata for the tool." + }, + "default_requires_approval": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Default Requires Approval", + "description": "Whether or not to require approval before executing this tool." + }, + "enable_parallel_execution": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Parallel Execution", + "description": "If set to True, then this tool will potentially be executed concurrently with other tools. Default False.", + "default": false + } + }, + "type": "object", + "title": "ToolUpdate" + }, + "TurnTokenData": { + "properties": { + "role": { + "type": "string", + "enum": ["assistant", "tool"], + "title": "Role", + "description": "Role of this turn: 'assistant' for LLM generations (trainable), 'tool' for tool results (non-trainable)." + }, + "output_ids": { + "anyOf": [ + { + "items": { + "type": "integer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Output Ids", + "description": "Token IDs from SGLang native endpoint. Only present for assistant turns." + }, + "output_token_logprobs": { + "anyOf": [ + { + "items": { + "items": {}, + "type": "array" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Output Token Logprobs", + "description": "Logprobs from SGLang: [[logprob, token_id, top_logprob_or_null], ...]. Only present for assistant turns." + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Content", + "description": "Text content. For tool turns, client tokenizes this with loss_mask=0." + }, + "tool_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tool Name", + "description": "Name of the tool called. Only present for tool turns." + } + }, + "type": "object", + "required": ["role"], + "title": "TurnTokenData", + "description": "Token data for a single LLM generation turn in a multi-turn agent interaction.\n\nUsed for RL training to track token IDs and logprobs across all LLM calls,\nnot just the final one. Tool results are included so the client can tokenize\nthem with loss_mask=0 (non-trainable)." + }, + "UpdateAgent": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "The name of the agent." + }, + "tool_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 41, + "minLength": 41, + "pattern": "^tool-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the tool in the format 'tool-'", + "examples": ["tool-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Ids", + "description": "The ids of the tools used by the agent." + }, + "source_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 43, + "minLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Source Ids", + "description": "Deprecated: Use `folder_ids` field instead. The ids of the sources used by the agent.", + "deprecated": true + }, + "folder_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 43, + "minLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Folder Ids", + "description": "The ids of the folders used by the agent." + }, + "block_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^block-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the block in the format 'block-'", + "examples": ["block-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Block Ids", + "description": "The ids of the blocks used by the agent." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "The tags associated with the agent." + }, + "system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "System", + "description": "The system prompt used by the agent." + }, + "tool_rules": { + "anyOf": [ + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ChildToolRule" + }, + { + "$ref": "#/components/schemas/InitToolRule" + }, + { + "$ref": "#/components/schemas/TerminalToolRule" + }, + { + "$ref": "#/components/schemas/ConditionalToolRule" + }, + { + "$ref": "#/components/schemas/ContinueToolRule" + }, + { + "$ref": "#/components/schemas/RequiredBeforeExitToolRule" + }, + { + "$ref": "#/components/schemas/MaxCountPerStepToolRule" + }, + { + "$ref": "#/components/schemas/ParentToolRule" + }, + { + "$ref": "#/components/schemas/RequiresApprovalToolRule" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "conditional": "#/components/schemas/ConditionalToolRule", + "constrain_child_tools": "#/components/schemas/ChildToolRule", + "continue_loop": "#/components/schemas/ContinueToolRule", + "exit_loop": "#/components/schemas/TerminalToolRule", + "max_count_per_step": "#/components/schemas/MaxCountPerStepToolRule", + "parent_last_tool": "#/components/schemas/ParentToolRule", + "required_before_exit": "#/components/schemas/RequiredBeforeExitToolRule", + "requires_approval": "#/components/schemas/RequiresApprovalToolRule", + "run_first": "#/components/schemas/InitToolRule" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Rules", + "description": "The tool rules governing the agent." + }, + "message_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 44, + "minLength": 44, + "pattern": "^message-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the message in the format 'message-'", + "examples": ["message-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Message Ids", + "description": "The ids of the messages in the agent's in-context memory." + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the agent." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "The metadata of the agent." + }, + "tool_exec_environment_variables": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Tool Exec Environment Variables", + "description": "Deprecated: use `secrets` field instead" + }, + "secrets": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Secrets", + "description": "The environment variables for tool execution specific to this agent." + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The id of the project the agent belongs to." + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "The id of the template the agent belongs to." + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "The base template id of the agent." + }, + "identity_ids": { + "anyOf": [ + { + "items": { + "type": "string", + "maxLength": 45, + "minLength": 45, + "pattern": "^identity-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Identity Ids", + "description": "The ids of the identities associated with this agent." + }, + "message_buffer_autoclear": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Message Buffer Autoclear", + "description": "If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case." + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "The model handle used by the agent (format: provider/model-name)." + }, + "embedding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Embedding", + "description": "The embedding model handle used by the agent (format: provider/model-name)." + }, + "model_settings": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/OpenAIModelSettings" + }, + { + "$ref": "#/components/schemas/SGLangModelSettings" + }, + { + "$ref": "#/components/schemas/AnthropicModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleAIModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleVertexModelSettings" + }, + { + "$ref": "#/components/schemas/AzureModelSettings" + }, + { + "$ref": "#/components/schemas/XAIModelSettings" + }, + { + "$ref": "#/components/schemas/ZAIModelSettings" + }, + { + "$ref": "#/components/schemas/GroqModelSettings" + }, + { + "$ref": "#/components/schemas/DeepseekModelSettings" + }, + { + "$ref": "#/components/schemas/TogetherModelSettings" + }, + { + "$ref": "#/components/schemas/BedrockModelSettings" + }, + { + "$ref": "#/components/schemas/BasetenModelSettings" + }, + { + "$ref": "#/components/schemas/OpenRouterModelSettings" + }, + { + "$ref": "#/components/schemas/ChatGPTOAuthModelSettings" + } + ], + "discriminator": { + "propertyName": "provider_type", + "mapping": { + "anthropic": "#/components/schemas/AnthropicModelSettings", + "azure": "#/components/schemas/AzureModelSettings", + "baseten": "#/components/schemas/BasetenModelSettings", + "bedrock": "#/components/schemas/BedrockModelSettings", + "chatgpt_oauth": "#/components/schemas/ChatGPTOAuthModelSettings", + "deepseek": "#/components/schemas/DeepseekModelSettings", + "google_ai": "#/components/schemas/GoogleAIModelSettings", + "google_vertex": "#/components/schemas/GoogleVertexModelSettings", + "groq": "#/components/schemas/GroqModelSettings", + "openai": "#/components/schemas/OpenAIModelSettings", + "openrouter": "#/components/schemas/OpenRouterModelSettings", + "sglang": "#/components/schemas/SGLangModelSettings", + "together": "#/components/schemas/TogetherModelSettings", + "xai": "#/components/schemas/XAIModelSettings", + "zai": "#/components/schemas/ZAIModelSettings" + } + } + }, + { + "type": "null" + } + ], + "title": "Model Settings", + "description": "The model settings for the agent." + }, + "compaction_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/CompactionSettings-Input" + }, + { + "type": "null" + } + ], + "description": "The compaction settings configuration used for compaction." + }, + "context_window_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Context Window Limit", + "description": "The context window limit used by the agent." + }, + "reasoning": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Reasoning", + "description": "Deprecated: Use `model` field to configure reasoning instead. Whether to enable reasoning for this agent.", + "deprecated": true + }, + "llm_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/LLMConfig" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use `model` field instead. The LLM configuration used by the agent.", + "deprecated": true + }, + "embedding_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + { + "type": "null" + } + ], + "description": "The embedding configuration used by the agent." + }, + "parallel_tool_calls": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Parallel Tool Calls", + "description": "Deprecated: Use `model_settings` to configure parallel tool calls instead. If set to True, enables parallel tool calling.", + "deprecated": true + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "Deprecated: Use `model_settings` field to configure response format instead. The response format for the agent.", + "deprecated": true + }, + "max_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Tokens", + "description": "Deprecated: Use `model` field to configure max output tokens instead. The maximum number of tokens to generate, including reasoning step.", + "deprecated": true + }, + "enable_sleeptime": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Sleeptime", + "description": "If set to True, memory management will move to a background agent thread." + }, + "last_run_completion": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Run Completion", + "description": "The timestamp when the agent last completed a run." + }, + "last_run_duration_ms": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Last Run Duration Ms", + "description": "The duration in milliseconds of the agent's last run." + }, + "last_stop_reason": { + "anyOf": [ + { + "$ref": "#/components/schemas/StopReasonType" + }, + { + "type": "null" + } + ], + "description": "The stop reason from the agent's last run." + }, + "timezone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The timezone of the agent (IANA format)." + }, + "max_files_open": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Files Open", + "description": "Maximum number of files that can be open at once for this agent. Setting this too high may exceed the context window, which will break the agent." + }, + "per_file_view_window_char_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Per File View Window Char Limit", + "description": "The per-file view window character limit for this agent. Setting this too high may exceed the context window, which will break the agent." + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "If set to True, the agent will be hidden." + } + }, + "type": "object", + "title": "UpdateAgent" + }, + "UpdateAssistantMessage": { + "properties": { + "message_type": { + "type": "string", + "const": "assistant_message", + "title": "Message Type", + "default": "assistant_message" + }, + "content": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/LettaAssistantMessageContentUnion" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "title": "Content", + "description": "The message content sent by the assistant (can be a string or an array of content parts)" + } + }, + "type": "object", + "required": ["content"], + "title": "UpdateAssistantMessage" + }, + "UpdateConversation": { + "properties": { + "summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Summary", + "description": "A summary of the conversation." + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "The model handle for this conversation (overrides agent's model). Format: provider/model-name." + }, + "model_settings": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/OpenAIModelSettings" + }, + { + "$ref": "#/components/schemas/SGLangModelSettings" + }, + { + "$ref": "#/components/schemas/AnthropicModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleAIModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleVertexModelSettings" + }, + { + "$ref": "#/components/schemas/AzureModelSettings" + }, + { + "$ref": "#/components/schemas/XAIModelSettings" + }, + { + "$ref": "#/components/schemas/ZAIModelSettings" + }, + { + "$ref": "#/components/schemas/GroqModelSettings" + }, + { + "$ref": "#/components/schemas/DeepseekModelSettings" + }, + { + "$ref": "#/components/schemas/TogetherModelSettings" + }, + { + "$ref": "#/components/schemas/BedrockModelSettings" + }, + { + "$ref": "#/components/schemas/BasetenModelSettings" + }, + { + "$ref": "#/components/schemas/OpenRouterModelSettings" + }, + { + "$ref": "#/components/schemas/ChatGPTOAuthModelSettings" + } + ], + "discriminator": { + "propertyName": "provider_type", + "mapping": { + "anthropic": "#/components/schemas/AnthropicModelSettings", + "azure": "#/components/schemas/AzureModelSettings", + "baseten": "#/components/schemas/BasetenModelSettings", + "bedrock": "#/components/schemas/BedrockModelSettings", + "chatgpt_oauth": "#/components/schemas/ChatGPTOAuthModelSettings", + "deepseek": "#/components/schemas/DeepseekModelSettings", + "google_ai": "#/components/schemas/GoogleAIModelSettings", + "google_vertex": "#/components/schemas/GoogleVertexModelSettings", + "groq": "#/components/schemas/GroqModelSettings", + "openai": "#/components/schemas/OpenAIModelSettings", + "openrouter": "#/components/schemas/OpenRouterModelSettings", + "sglang": "#/components/schemas/SGLangModelSettings", + "together": "#/components/schemas/TogetherModelSettings", + "xai": "#/components/schemas/XAIModelSettings", + "zai": "#/components/schemas/ZAIModelSettings" + } + } + }, + { + "type": "null" + } + ], + "title": "Model Settings", + "description": "The model settings for this conversation (overrides agent's model settings)." + }, + "last_message_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Message At", + "description": "Timestamp of the most recent message request sent to this conversation." + } + }, + "type": "object", + "title": "UpdateConversation", + "description": "Request model for updating a conversation." + }, + "UpdateMCPServerRequest": { + "properties": { + "server_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Server Name", + "description": "The name of the MCP server" + }, + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/letta__schemas__mcp_server__UpdateStdioMCPServer" + }, + { + "$ref": "#/components/schemas/letta__schemas__mcp_server__UpdateSSEMCPServer" + }, + { + "$ref": "#/components/schemas/letta__schemas__mcp_server__UpdateStreamableHTTPMCPServer" + } + ], + "title": "Config", + "description": "The MCP server configuration updates (Stdio, SSE, or Streamable HTTP)", + "discriminator": { + "propertyName": "mcp_server_type", + "mapping": { + "sse": "#/components/schemas/letta__schemas__mcp_server__UpdateSSEMCPServer", + "stdio": "#/components/schemas/letta__schemas__mcp_server__UpdateStdioMCPServer", + "streamable_http": "#/components/schemas/letta__schemas__mcp_server__UpdateStreamableHTTPMCPServer" + } + } + } + }, + "additionalProperties": false, + "type": "object", + "required": ["config"], + "title": "UpdateMCPServerRequest", + "description": "Request to update an existing MCP server configuration." + }, + "UpdateReasoningMessage": { + "properties": { + "reasoning": { + "type": "string", + "title": "Reasoning" + }, + "message_type": { + "type": "string", + "const": "reasoning_message", + "title": "Message Type", + "default": "reasoning_message" + } + }, + "type": "object", + "required": ["reasoning"], + "title": "UpdateReasoningMessage" + }, + "UpdateSystemMessage": { + "properties": { + "message_type": { + "type": "string", + "const": "system_message", + "title": "Message Type", + "default": "system_message" + }, + "content": { + "type": "string", + "title": "Content", + "description": "The message content sent by the system (can be a string or an array of multi-modal content parts)" + } + }, + "type": "object", + "required": ["content"], + "title": "UpdateSystemMessage" + }, + "UpdateUserMessage": { + "properties": { + "message_type": { + "type": "string", + "const": "user_message", + "title": "Message Type", + "default": "user_message" + }, + "content": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/LettaUserMessageContentUnion" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "title": "Content", + "description": "The message content sent by the user (can be a string or an array of multi-modal content parts)" + } + }, + "type": "object", + "required": ["content"], + "title": "UpdateUserMessage" + }, + "UrlImage": { + "properties": { + "type": { + "type": "string", + "const": "url", + "title": "Type", + "description": "The source type for the image.", + "default": "url" + }, + "url": { + "type": "string", + "title": "Url", + "description": "The URL of the image." + } + }, + "type": "object", + "required": ["url"], + "title": "UrlImage" + }, + "UsageStatistics": { + "properties": { + "completion_tokens": { + "type": "integer", + "title": "Completion Tokens", + "default": 0 + }, + "prompt_tokens": { + "type": "integer", + "title": "Prompt Tokens", + "default": 0 + }, + "total_tokens": { + "type": "integer", + "title": "Total Tokens", + "default": 0 + }, + "prompt_tokens_details": { + "anyOf": [ + { + "$ref": "#/components/schemas/UsageStatisticsPromptTokenDetails" + }, + { + "type": "null" + } + ] + }, + "completion_tokens_details": { + "anyOf": [ + { + "$ref": "#/components/schemas/UsageStatisticsCompletionTokenDetails" + }, + { + "type": "null" + } + ] + } + }, + "type": "object", + "title": "UsageStatistics" + }, + "UsageStatisticsCompletionTokenDetails": { + "properties": { + "reasoning_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Reasoning Tokens" + } + }, + "type": "object", + "title": "UsageStatisticsCompletionTokenDetails" + }, + "UsageStatisticsPromptTokenDetails": { + "properties": { + "cached_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Cached Tokens" + }, + "cache_read_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Cache Read Tokens" + }, + "cache_creation_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Cache Creation Tokens" + } + }, + "type": "object", + "title": "UsageStatisticsPromptTokenDetails" + }, + "User": { + "properties": { + "id": { + "type": "string", + "pattern": "^user-[a-fA-F0-9]{8}", + "title": "Id", + "description": "The human-friendly ID of the User", + "examples": ["user-123e4567-e89b-12d3-a456-426614174000"] + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the user." + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At", + "description": "The creation date of the user." + }, + "updated_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Updated At", + "description": "The update date of the user." + }, + "is_deleted": { + "type": "boolean", + "title": "Is Deleted", + "description": "Whether this user is deleted or not.", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "required": ["name"], + "title": "User", + "description": "Representation of a user." + }, + "UserCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the user." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["name", "organization_id"], + "title": "UserCreate" + }, + "UserMessage": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "date": { + "type": "string", + "format": "date-time", + "title": "Date" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "message_type": { + "type": "string", + "const": "user_message", + "title": "Message Type", + "description": "The type of the message.", + "default": "user_message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id" + }, + "step_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Step Id" + }, + "is_err": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Err" + }, + "seq_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Seq Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Run Id" + }, + "content": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/LettaUserMessageContentUnion" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "title": "Content", + "description": "The message content sent by the user (can be a string or an array of multi-modal content parts)" + } + }, + "type": "object", + "required": ["id", "date", "content"], + "title": "UserMessage", + "description": "A message sent by the user. Never streamed back on a response, only used for cursor pagination.\n\nArgs:\n id (str): The ID of the message\n date (datetime): The date the message was created in ISO format\n name (Optional[str]): The name of the sender of the message\n content (Union[str, List[LettaUserMessageContentUnion]]): The message content sent by the user (can be a string or an array of multi-modal content parts)" + }, + "UserMessageListResult": { + "properties": { + "message_type": { + "type": "string", + "const": "user_message", + "title": "Message Type", + "default": "user_message" + }, + "content": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/LettaUserMessageContentUnion" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "title": "Content", + "description": "The message content sent by the user (can be a string or an array of multi-modal content parts)" + }, + "message_id": { + "type": "string", + "title": "Message Id", + "description": "The unique identifier of the message." + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "The unique identifier of the agent that owns the message." + }, + "conversation_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Conversation Id", + "description": "The unique identifier of the conversation that the message belongs to." + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The time the message was created in ISO format." + } + }, + "type": "object", + "required": ["content", "message_id", "created_at"], + "title": "UserMessageListResult", + "description": "User message list result with agent context.\n\nShape is identical to UpdateUserMessage but includes the owning agent_id and message id." + }, + "UserUpdate": { + "properties": { + "id": { + "type": "string", + "maxLength": 41, + "minLength": 41, + "pattern": "^user-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "title": "Id", + "description": "The id of the user to update.", + "examples": ["user-123e4567-e89b-42d3-8456-426614174000"] + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "The new name of the user." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["id"], + "title": "UserUpdate" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError" + }, + "VectorDBProvider": { + "type": "string", + "enum": ["native", "tpuf", "pinecone"], + "title": "VectorDBProvider", + "description": "Supported vector database providers for archival memory" + }, + "VoiceSleeptimeManager": { + "properties": { + "manager_type": { + "type": "string", + "const": "voice_sleeptime", + "title": "Manager Type", + "description": "", + "default": "voice_sleeptime" + }, + "manager_agent_id": { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "title": "Manager Agent Id", + "description": "", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + "max_message_buffer_length": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Message Buffer Length", + "description": "The desired maximum length of messages in the context window of the convo agent. This is a best effort, and may be off slightly due to user/assistant interleaving." + }, + "min_message_buffer_length": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Min Message Buffer Length", + "description": "The desired minimum length of messages in the context window of the convo agent. This is a best effort, and may be off-by-one due to user/assistant interleaving." + } + }, + "type": "object", + "required": ["manager_agent_id"], + "title": "VoiceSleeptimeManager" + }, + "VoiceSleeptimeManagerSchema": { + "properties": { + "manager_type": { + "type": "string", + "const": "voice_sleeptime", + "title": "Manager Type", + "description": "", + "default": "voice_sleeptime" + }, + "manager_agent_id": { + "type": "string", + "title": "Manager Agent Id", + "description": "" + }, + "max_message_buffer_length": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Message Buffer Length", + "description": "" + }, + "min_message_buffer_length": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Min Message Buffer Length", + "description": "" + } + }, + "type": "object", + "required": ["manager_agent_id"], + "title": "VoiceSleeptimeManagerSchema" + }, + "VoiceSleeptimeManagerUpdate": { + "properties": { + "manager_type": { + "type": "string", + "const": "voice_sleeptime", + "title": "Manager Type", + "description": "", + "default": "voice_sleeptime" + }, + "manager_agent_id": { + "anyOf": [ + { + "type": "string", + "maxLength": 42, + "minLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"] + }, + { + "type": "null" + } + ], + "title": "Manager Agent Id", + "description": "" + }, + "max_message_buffer_length": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Message Buffer Length", + "description": "The desired maximum length of messages in the context window of the convo agent. This is a best effort, and may be off slightly due to user/assistant interleaving." + }, + "min_message_buffer_length": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Min Message Buffer Length", + "description": "The desired minimum length of messages in the context window of the convo agent. This is a best effort, and may be off-by-one due to user/assistant interleaving." + } + }, + "type": "object", + "title": "VoiceSleeptimeManagerUpdate" + }, + "XAIModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "xai", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "xai" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model." + } + }, + "type": "object", + "title": "XAIModelSettings", + "description": "xAI model configuration (OpenAI-compatible)." + }, + "ZAIModelSettings": { + "properties": { + "max_output_tokens": { + "type": "integer", + "title": "Max Output Tokens", + "description": "The maximum number of tokens the model can generate.", + "default": 4096 + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "Whether to enable parallel tool calling.", + "default": true + }, + "provider_type": { + "type": "string", + "const": "zai", + "title": "Provider Type", + "description": "The type of the provider.", + "default": "zai" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The temperature of the model.", + "default": 0.7 + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "The response format for the model." + }, + "thinking": { + "$ref": "#/components/schemas/ZAIThinking", + "description": "The thinking configuration for GLM-4.5+ models.", + "default": { + "type": "enabled", + "clear_thinking": false + } + } + }, + "type": "object", + "title": "ZAIModelSettings", + "description": "Z.ai (ZhipuAI) model configuration (OpenAI-compatible)." + }, + "ZAIThinking": { + "properties": { + "type": { + "type": "string", + "enum": ["enabled", "disabled"], + "title": "Type", + "description": "Whether thinking is enabled or disabled.", + "default": "enabled" + }, + "clear_thinking": { + "type": "boolean", + "title": "Clear Thinking", + "description": "If False, preserved thinking is used (recommended for agents).", + "default": false + } + }, + "type": "object", + "title": "ZAIThinking", + "description": "Thinking configuration for ZAI GLM-4.5+ models." + }, + "letta__schemas__agent_file__AgentSchema": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the agent." + }, + "memory_blocks": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/CreateBlock" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Memory Blocks", + "description": "The blocks to create in the agent's in-context memory." + }, + "tools": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tools", + "description": "The tools used by the agent." + }, + "tool_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Ids", + "description": "The ids of the tools used by the agent." + }, + "source_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Source Ids", + "description": "The ids of the sources used by the agent." + }, + "folder_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Folder Ids", + "description": "The ids of the folders used by the agent." + }, + "block_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Block Ids", + "description": "The ids of the blocks used by the agent." + }, + "tool_rules": { + "anyOf": [ + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ChildToolRule" + }, + { + "$ref": "#/components/schemas/InitToolRule" + }, + { + "$ref": "#/components/schemas/TerminalToolRule" + }, + { + "$ref": "#/components/schemas/ConditionalToolRule" + }, + { + "$ref": "#/components/schemas/ContinueToolRule" + }, + { + "$ref": "#/components/schemas/RequiredBeforeExitToolRule" + }, + { + "$ref": "#/components/schemas/MaxCountPerStepToolRule" + }, + { + "$ref": "#/components/schemas/ParentToolRule" + }, + { + "$ref": "#/components/schemas/RequiresApprovalToolRule" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "conditional": "#/components/schemas/ConditionalToolRule", + "constrain_child_tools": "#/components/schemas/ChildToolRule", + "continue_loop": "#/components/schemas/ContinueToolRule", + "exit_loop": "#/components/schemas/TerminalToolRule", + "max_count_per_step": "#/components/schemas/MaxCountPerStepToolRule", + "parent_last_tool": "#/components/schemas/ParentToolRule", + "required_before_exit": "#/components/schemas/RequiredBeforeExitToolRule", + "requires_approval": "#/components/schemas/RequiresApprovalToolRule", + "run_first": "#/components/schemas/InitToolRule" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Rules", + "description": "The tool rules governing the agent." + }, + "tags": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tags", + "description": "The tags associated with the agent." + }, + "system": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "System", + "description": "The system prompt used by the agent." + }, + "agent_type": { + "$ref": "#/components/schemas/AgentType", + "description": "The type of agent." + }, + "initial_message_sequence": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/MessageCreate" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Initial Message Sequence", + "description": "The initial set of messages to put in the agent's in-context memory." + }, + "include_base_tools": { + "type": "boolean", + "title": "Include Base Tools", + "description": "If true, attaches the Letta core tools (e.g. core_memory related functions).", + "default": true + }, + "include_multi_agent_tools": { + "type": "boolean", + "title": "Include Multi Agent Tools", + "description": "If true, attaches the Letta multi-agent tools (e.g. sending a message to another agent).", + "default": false + }, + "include_base_tool_rules": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Include Base Tool Rules", + "description": "If true, attaches the Letta base tool rules (e.g. deny all tools not explicitly allowed)." + }, + "include_default_source": { + "type": "boolean", + "title": "Include Default Source", + "description": "If true, automatically creates and attaches a default data source for this agent.", + "default": false, + "deprecated": true + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the agent." + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "The metadata of the agent." + }, + "llm_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/LLMConfig" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use `model` field instead. The LLM configuration used by the agent.", + "deprecated": true + }, + "embedding_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + { + "type": "null" + } + ], + "description": "Deprecated: Use `embedding` field instead. The embedding configuration used by the agent.", + "deprecated": true + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "The model handle for the agent to use (format: provider/model-name)." + }, + "embedding": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Embedding", + "description": "The embedding model handle used by the agent (format: provider/model-name)." + }, + "model_settings": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/OpenAIModelSettings" + }, + { + "$ref": "#/components/schemas/SGLangModelSettings" + }, + { + "$ref": "#/components/schemas/AnthropicModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleAIModelSettings" + }, + { + "$ref": "#/components/schemas/GoogleVertexModelSettings" + }, + { + "$ref": "#/components/schemas/AzureModelSettings" + }, + { + "$ref": "#/components/schemas/XAIModelSettings" + }, + { + "$ref": "#/components/schemas/ZAIModelSettings" + }, + { + "$ref": "#/components/schemas/GroqModelSettings" + }, + { + "$ref": "#/components/schemas/DeepseekModelSettings" + }, + { + "$ref": "#/components/schemas/TogetherModelSettings" + }, + { + "$ref": "#/components/schemas/BedrockModelSettings" + }, + { + "$ref": "#/components/schemas/BasetenModelSettings" + }, + { + "$ref": "#/components/schemas/OpenRouterModelSettings" + }, + { + "$ref": "#/components/schemas/ChatGPTOAuthModelSettings" + } + ], + "discriminator": { + "propertyName": "provider_type", + "mapping": { + "anthropic": "#/components/schemas/AnthropicModelSettings", + "azure": "#/components/schemas/AzureModelSettings", + "baseten": "#/components/schemas/BasetenModelSettings", + "bedrock": "#/components/schemas/BedrockModelSettings", + "chatgpt_oauth": "#/components/schemas/ChatGPTOAuthModelSettings", + "deepseek": "#/components/schemas/DeepseekModelSettings", + "google_ai": "#/components/schemas/GoogleAIModelSettings", + "google_vertex": "#/components/schemas/GoogleVertexModelSettings", + "groq": "#/components/schemas/GroqModelSettings", + "openai": "#/components/schemas/OpenAIModelSettings", + "openrouter": "#/components/schemas/OpenRouterModelSettings", + "sglang": "#/components/schemas/SGLangModelSettings", + "together": "#/components/schemas/TogetherModelSettings", + "xai": "#/components/schemas/XAIModelSettings", + "zai": "#/components/schemas/ZAIModelSettings" + } + } + }, + { + "type": "null" + } + ], + "title": "Model Settings", + "description": "The model settings for the agent." + }, + "compaction_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/CompactionSettings-Input" + }, + { + "type": "null" + } + ], + "description": "The compaction settings configuration used for compaction." + }, + "context_window_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Context Window Limit", + "description": "The context window limit used by the agent." + }, + "embedding_chunk_size": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Embedding Chunk Size", + "description": "Deprecated: No longer used. The embedding chunk size used by the agent.", + "default": 300, + "deprecated": true + }, + "max_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Tokens", + "description": "Deprecated: Use `model` field to configure max output tokens instead. The maximum number of tokens to generate, including reasoning step.", + "deprecated": true + }, + "max_reasoning_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Reasoning Tokens", + "description": "Deprecated: Use `model` field to configure reasoning tokens instead. The maximum number of tokens to generate for reasoning step.", + "deprecated": true + }, + "enable_reasoner": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Reasoner", + "description": "Deprecated: Use `model` field to configure reasoning instead. Whether to enable internal extended thinking step for a reasoner model.", + "default": true, + "deprecated": true + }, + "reasoning": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Reasoning", + "description": "Deprecated: Use `model` field to configure reasoning instead. Whether to enable reasoning for this agent.", + "deprecated": true + }, + "from_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "From Template", + "description": "Deprecated: please use the 'create agents from a template' endpoint instead.", + "deprecated": true + }, + "template": { + "type": "boolean", + "title": "Template", + "description": "Deprecated: No longer used.", + "default": false, + "deprecated": true + }, + "project": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project", + "description": "Deprecated: Project should now be passed via the X-Project header instead of in the request body. If using the SDK, this can be done via the x_project parameter.", + "deprecated": true + }, + "tool_exec_environment_variables": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Tool Exec Environment Variables", + "description": "Deprecated: Use `secrets` field instead. Environment variables for tool execution.", + "deprecated": true + }, + "secrets": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Secrets", + "description": "The environment variables for tool execution specific to this agent." + }, + "memory_variables": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Memory Variables", + "description": "Deprecated: Only relevant for creating agents from a template. Use the 'create agents from a template' endpoint instead.", + "deprecated": true + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "Deprecated: No longer used. The id of the project the agent belongs to.", + "deprecated": true + }, + "template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Template Id", + "description": "Deprecated: No longer used. The id of the template the agent belongs to.", + "deprecated": true + }, + "base_template_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base Template Id", + "description": "Deprecated: No longer used. The base template id of the agent.", + "deprecated": true + }, + "identity_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Identity Ids", + "description": "The ids of the identities associated with this agent." + }, + "message_buffer_autoclear": { + "type": "boolean", + "title": "Message Buffer Autoclear", + "description": "If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case.", + "default": false + }, + "enable_sleeptime": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Sleeptime", + "description": "If set to True, memory management will move to a background agent thread." + }, + "response_format": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/TextResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonSchemaResponseFormat" + }, + { + "$ref": "#/components/schemas/JsonObjectResponseFormat" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "json_object": "#/components/schemas/JsonObjectResponseFormat", + "json_schema": "#/components/schemas/JsonSchemaResponseFormat", + "text": "#/components/schemas/TextResponseFormat" + } + } + }, + { + "type": "null" + } + ], + "title": "Response Format", + "description": "Deprecated: Use `model_settings` field to configure response format instead. The response format for the agent.", + "deprecated": true + }, + "timezone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Timezone", + "description": "The timezone of the agent (IANA format)." + }, + "max_files_open": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Files Open", + "description": "Maximum number of files that can be open at once for this agent. Setting this too high may exceed the context window, which will break the agent." + }, + "per_file_view_window_char_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Per File View Window Char Limit", + "description": "The per-file view window character limit for this agent. Setting this too high may exceed the context window, which will break the agent." + }, + "hidden": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Hidden", + "description": "Deprecated: No longer used. If set to True, the agent will be hidden.", + "deprecated": true + }, + "parallel_tool_calls": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Parallel Tool Calls", + "description": "Deprecated: Use `model_settings` to configure parallel tool calls instead. If set to True, enables parallel tool calling.", + "deprecated": true + }, + "id": { + "type": "string", + "title": "Id", + "description": "Human-readable identifier for this agent in the file" + }, + "in_context_message_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "In Context Message Ids", + "description": "List of message IDs that are currently in the agent's context" + }, + "messages": { + "items": { + "$ref": "#/components/schemas/letta__schemas__agent_file__MessageSchema" + }, + "type": "array", + "title": "Messages", + "description": "List of messages in the agent's conversation history" + }, + "files_agents": { + "items": { + "$ref": "#/components/schemas/FileAgentSchema" + }, + "type": "array", + "title": "Files Agents", + "description": "List of file-agent relationships for this agent" + }, + "group_ids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Group Ids", + "description": "List of groups that the agent manages" + } + }, + "type": "object", + "required": ["id"], + "title": "AgentSchema", + "description": "Agent with human-readable ID for agent file" + }, + "letta__schemas__agent_file__MessageSchema": { + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "message" + }, + { + "type": "null" + } + ], + "title": "Type", + "description": "The message type to be created.", + "default": "message" + }, + "otid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Otid", + "description": "The offline threading id (OTID). Set by the client to deduplicate requests. Used for idempotency in background streaming mode — each message in a request must have a unique OTID. Retries of the same request should reuse the same OTIDs." + }, + "group_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Group Id", + "description": "The multi-agent group that the message was sent in" + }, + "role": { + "$ref": "#/components/schemas/MessageRole", + "description": "The role of the participant." + }, + "content": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/LettaMessageContentUnion" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "title": "Content", + "description": "The content of the message." + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "The name of the participant." + }, + "sender_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sender Id", + "description": "The id of the sender of the message, can be an identity id or agent id" + }, + "batch_item_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Batch Item Id", + "description": "The id of the LLMBatchItem that this message is associated with" + }, + "id": { + "type": "string", + "title": "Id", + "description": "Human-readable identifier for this message in the file" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model", + "description": "The model used to make the function call" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "The unique identifier of the agent" + }, + "tool_calls": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/ChatCompletionMessageFunctionToolCall-Input" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Calls", + "description": "The list of tool calls requested. Only applicable for role assistant." + }, + "tool_call_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tool Call Id", + "description": "The ID of the tool call. Only applicable for role tool." + }, + "tool_returns": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/letta__schemas__message__ToolReturn-Input" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Tool Returns", + "description": "Tool execution return information for prior tool calls" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "The timestamp when the object was created." + }, + "approve": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Approve", + "description": "Whether the tool has been approved" + }, + "approval_request_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Approval Request Id", + "description": "The message ID of the approval request" + }, + "denial_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Denial Reason", + "description": "An optional explanation for the provided approval status" + }, + "approvals": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApprovalReturn" + }, + { + "$ref": "#/components/schemas/letta__schemas__message__ToolReturn-Input" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Approvals", + "description": "Approval returns for the message" + } + }, + "type": "object", + "required": ["role", "content", "id"], + "title": "MessageSchema", + "description": "Message with human-readable ID for agent file" + }, + "letta__schemas__agent_file__ToolSchema": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Human-readable identifier for this tool in the file" + }, + "tool_type": { + "$ref": "#/components/schemas/ToolType", + "description": "The type of the tool.", + "default": "custom" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description", + "description": "The description of the tool." + }, + "source_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Type", + "description": "The type of the source code." + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name", + "description": "The name of the function." + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags", + "description": "Metadata tags.", + "default": [] + }, + "source_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Code", + "description": "The source code of the function." + }, + "json_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Json Schema", + "description": "The JSON schema of the function." + }, + "args_json_schema": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Args Json Schema", + "description": "The args JSON schema of the function." + }, + "return_char_limit": { + "type": "integer", + "maximum": 1000000, + "minimum": 1, + "title": "Return Char Limit", + "description": "The maximum number of characters in the response.", + "default": 50000 + }, + "pip_requirements": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/PipRequirement" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Pip Requirements", + "description": "Optional list of pip packages required by this tool." + }, + "npm_requirements": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/NpmRequirement" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Npm Requirements", + "description": "Optional list of npm packages required by this tool." + }, + "default_requires_approval": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Default Requires Approval", + "description": "Default value for whether or not executing this tool requires approval." + }, + "enable_parallel_execution": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Enable Parallel Execution", + "description": "If set to True, then this tool will potentially be executed concurrently with other tools. Default False.", + "default": false + }, + "created_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Created By Id", + "description": "The id of the user that made this Tool." + }, + "last_updated_by_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Updated By Id", + "description": "The id of the user that made this Tool." + }, + "metadata_": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata", + "description": "A dictionary of additional metadata for the tool." + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project Id", + "description": "The project id of the tool." + } + }, + "additionalProperties": false, + "type": "object", + "required": ["id"], + "title": "ToolSchema", + "description": "Tool with human-readable ID for agent file" + }, + "letta__schemas__letta_message__ToolReturn": { + "properties": { + "type": { + "type": "string", + "const": "tool", + "title": "Type", + "description": "The message type to be created.", + "default": "tool" + }, + "tool_return": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/LettaToolReturnContentUnion" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "title": "Tool Return", + "description": "The tool return value - either a string or list of content parts (text/image)" + }, + "status": { + "type": "string", + "enum": ["success", "error"], + "title": "Status" + }, + "tool_call_id": { + "type": "string", + "title": "Tool Call Id" + }, + "stdout": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Stdout" + }, + "stderr": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Stderr" + } + }, + "type": "object", + "required": ["tool_return", "status", "tool_call_id"], + "title": "ToolReturn" + }, + "letta__schemas__mcp__UpdateSSEMCPServer": { + "properties": { + "server_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Server Name", + "description": "The name of the MCP server" + }, + "server_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Server Url", + "description": "The URL of the server (MCP SSE client will connect to this URL)" + }, + "token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Token", + "description": "The access token or API key for the MCP server (used for SSE authentication)" + }, + "custom_headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Headers", + "description": "Custom authentication headers as key-value pairs" + } + }, + "additionalProperties": false, + "type": "object", + "title": "UpdateSSEMCPServer", + "description": "Update an SSE MCP server" + }, + "letta__schemas__mcp__UpdateStdioMCPServer": { + "properties": { + "server_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Server Name", + "description": "The name of the MCP server" + }, + "stdio_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/StdioServerConfig" + }, + { + "type": "null" + } + ], + "description": "The configuration for the server (MCP 'local' client will run this command)" + } + }, + "additionalProperties": false, + "type": "object", + "title": "UpdateStdioMCPServer", + "description": "Update a Stdio MCP server" + }, + "letta__schemas__mcp__UpdateStreamableHTTPMCPServer": { + "properties": { + "server_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Server Name", + "description": "The name of the MCP server" + }, + "server_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Server Url", + "description": "The URL path for the streamable HTTP server (e.g., 'example/mcp')" + }, + "auth_header": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Header", + "description": "The name of the authentication header (e.g., 'Authorization')" + }, + "auth_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Token", + "description": "The authentication token or API key value" + }, + "custom_headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Headers", + "description": "Custom authentication headers as key-value pairs" + } + }, + "additionalProperties": false, + "type": "object", + "title": "UpdateStreamableHTTPMCPServer", + "description": "Update a Streamable HTTP MCP server" + }, + "letta__schemas__mcp_server__ToolExecuteRequest": { + "properties": { + "args": { + "additionalProperties": true, + "type": "object", + "title": "Args", + "description": "Arguments to pass to the tool" + } + }, + "additionalProperties": false, + "type": "object", + "title": "ToolExecuteRequest", + "description": "Request to execute a tool." + }, + "letta__schemas__mcp_server__UpdateSSEMCPServer": { + "properties": { + "mcp_server_type": { + "type": "string", + "const": "sse", + "title": "Mcp Server Type", + "default": "sse" + }, + "server_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Server Url", + "description": "The URL of the server" + }, + "auth_header": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Header", + "description": "The name of the authentication header (e.g., 'Authorization')" + }, + "auth_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Token", + "description": "The authentication token or API key value" + }, + "custom_headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Headers", + "description": "Custom HTTP headers to include with requests" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["server_url"], + "title": "UpdateSSEMCPServer", + "description": "Update schema for SSE MCP server - all fields optional" + }, + "letta__schemas__mcp_server__UpdateStdioMCPServer": { + "properties": { + "mcp_server_type": { + "type": "string", + "const": "stdio", + "title": "Mcp Server Type", + "default": "stdio" + }, + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Command", + "description": "The command to run (MCP 'local' client will run this command)" + }, + "args": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Args", + "description": "The arguments to pass to the command" + }, + "env": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Env", + "description": "Environment variables to set" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["command", "args"], + "title": "UpdateStdioMCPServer", + "description": "Update schema for Stdio MCP server - all fields optional" + }, + "letta__schemas__mcp_server__UpdateStreamableHTTPMCPServer": { + "properties": { + "mcp_server_type": { + "type": "string", + "const": "streamable_http", + "title": "Mcp Server Type", + "default": "streamable_http" + }, + "server_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Server Url", + "description": "The URL of the server" + }, + "auth_header": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Header", + "description": "The name of the authentication header (e.g., 'Authorization')" + }, + "auth_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Auth Token", + "description": "The authentication token or API key value" + }, + "custom_headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Custom Headers", + "description": "Custom HTTP headers to include with requests" + } + }, + "additionalProperties": false, + "type": "object", + "required": ["server_url"], + "title": "UpdateStreamableHTTPMCPServer", + "description": "Update schema for Streamable HTTP MCP server - all fields optional" + }, + "letta__schemas__message__ToolReturn-Input": { + "properties": { + "tool_call_id": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "title": "Tool Call Id", + "description": "The ID for the tool call" + }, + "status": { + "type": "string", + "enum": ["success", "error"], + "title": "Status", + "description": "The status of the tool call" + }, + "stdout": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Stdout", + "description": "Captured stdout (e.g. prints, logs) from the tool invocation" + }, + "stderr": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Stderr", + "description": "Captured stderr from the tool invocation" + }, + "func_response": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "$ref": "#/components/schemas/ImageContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "image": "#/components/schemas/ImageContent", + "text": "#/components/schemas/TextContent" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Func Response", + "description": "The function response - either a string or list of content parts (text/image)" + } + }, + "type": "object", + "required": ["status"], + "title": "ToolReturn" + }, + "letta__schemas__message__ToolReturn-Output": { + "properties": { + "tool_call_id": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "title": "Tool Call Id", + "description": "The ID for the tool call" + }, + "status": { + "type": "string", + "enum": ["success", "error"], + "title": "Status", + "description": "The status of the tool call" + }, + "stdout": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Stdout", + "description": "Captured stdout (e.g. prints, logs) from the tool invocation" + }, + "stderr": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Stderr", + "description": "Captured stderr from the tool invocation" + }, + "func_response": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "$ref": "#/components/schemas/ImageContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "image": "#/components/schemas/ImageContent", + "text": "#/components/schemas/TextContent" + } + } + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Func Response", + "description": "The function response - either a string or list of content parts (text/image)" + } + }, + "type": "object", + "required": ["status"], + "title": "ToolReturn" + }, + "letta__schemas__openai__chat_completion_response__ChatCompletionTokenLogprob": { + "properties": { + "token": { + "type": "string", + "title": "Token" + }, + "bytes": { + "anyOf": [ + { + "items": { + "type": "integer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Bytes" + }, + "logprob": { + "type": "number", + "title": "Logprob" + }, + "top_logprobs": { + "items": { + "$ref": "#/components/schemas/letta__schemas__openai__chat_completion_response__TopLogprob" + }, + "type": "array", + "title": "Top Logprobs" + } + }, + "type": "object", + "required": ["token", "logprob", "top_logprobs"], + "title": "ChatCompletionTokenLogprob" + }, + "letta__schemas__openai__chat_completion_response__ChoiceLogprobs": { + "properties": { + "content": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/letta__schemas__openai__chat_completion_response__ChatCompletionTokenLogprob" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Content" + }, + "refusal": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/letta__schemas__openai__chat_completion_response__ChatCompletionTokenLogprob" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Refusal" + } + }, + "type": "object", + "title": "ChoiceLogprobs" + }, + "letta__schemas__openai__chat_completion_response__TopLogprob": { + "properties": { + "token": { + "type": "string", + "title": "Token" + }, + "bytes": { + "anyOf": [ + { + "items": { + "type": "integer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Bytes" + }, + "logprob": { + "type": "number", + "title": "Logprob" + } + }, + "type": "object", + "required": ["token", "logprob"], + "title": "TopLogprob" + }, + "letta__serialize_schemas__pydantic_agent_schema__AgentSchema": { + "properties": { + "agent_type": { + "type": "string", + "title": "Agent Type" + }, + "core_memory": { + "items": { + "$ref": "#/components/schemas/CoreMemoryBlockSchema" + }, + "type": "array", + "title": "Core Memory" + }, + "created_at": { + "type": "string", + "title": "Created At" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "embedding_config": { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + "llm_config": { + "$ref": "#/components/schemas/LLMConfig" + }, + "message_buffer_autoclear": { + "type": "boolean", + "title": "Message Buffer Autoclear" + }, + "in_context_message_indices": { + "items": { + "type": "integer" + }, + "type": "array", + "title": "In Context Message Indices" + }, + "messages": { + "items": { + "$ref": "#/components/schemas/letta__serialize_schemas__pydantic_agent_schema__MessageSchema" + }, + "type": "array", + "title": "Messages" + }, + "metadata_": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" + }, + "multi_agent_group": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "title": "Multi Agent Group" + }, + "name": { + "type": "string", + "title": "Name" + }, + "system": { + "type": "string", + "title": "System" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/TagSchema" + }, + "type": "array", + "title": "Tags" + }, + "tool_exec_environment_variables": { + "items": { + "$ref": "#/components/schemas/ToolEnvVarSchema" + }, + "type": "array", + "title": "Tool Exec Environment Variables" + }, + "tool_rules": { + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/BaseToolRuleSchema" + }, + { + "$ref": "#/components/schemas/ChildToolRuleSchema" + }, + { + "$ref": "#/components/schemas/MaxCountPerStepToolRuleSchema" + }, + { + "$ref": "#/components/schemas/ConditionalToolRuleSchema" + } + ] + }, + "type": "array", + "title": "Tool Rules" + }, + "tools": { + "items": { + "$ref": "#/components/schemas/letta__serialize_schemas__pydantic_agent_schema__ToolSchema" + }, + "type": "array", + "title": "Tools" + }, + "updated_at": { + "type": "string", + "title": "Updated At" + }, + "version": { + "type": "string", + "title": "Version" + } + }, + "type": "object", + "required": [ + "agent_type", + "core_memory", + "created_at", + "description", + "embedding_config", + "llm_config", + "message_buffer_autoclear", + "in_context_message_indices", + "messages", + "multi_agent_group", + "name", + "system", + "tags", + "tool_exec_environment_variables", + "tool_rules", + "tools", + "updated_at", + "version" + ], + "title": "AgentSchema" + }, + "letta__serialize_schemas__pydantic_agent_schema__MessageSchema": { + "properties": { + "created_at": { + "type": "string", + "title": "Created At" + }, + "group_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Group Id" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Model" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "role": { + "type": "string", + "title": "Role" + }, + "content": { + "items": { + "$ref": "#/components/schemas/LettaMessageContentUnion" + }, + "type": "array", + "title": "Content" + }, + "tool_call_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tool Call Id" + }, + "tool_calls": { + "items": {}, + "type": "array", + "title": "Tool Calls" + }, + "tool_returns": { + "items": {}, + "type": "array", + "title": "Tool Returns" + }, + "updated_at": { + "type": "string", + "title": "Updated At" + } + }, + "type": "object", + "required": [ + "created_at", + "group_id", + "model", + "name", + "role", + "content", + "tool_call_id", + "tool_calls", + "tool_returns", + "updated_at" + ], + "title": "MessageSchema" + }, + "letta__serialize_schemas__pydantic_agent_schema__ToolSchema": { + "properties": { + "args_json_schema": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "title": "Args Json Schema" + }, + "created_at": { + "type": "string", + "title": "Created At" + }, + "description": { + "type": "string", + "title": "Description" + }, + "json_schema": { + "$ref": "#/components/schemas/ToolJSONSchema" + }, + "name": { + "type": "string", + "title": "Name" + }, + "return_char_limit": { + "type": "integer", + "title": "Return Char Limit" + }, + "source_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Source Code" + }, + "source_type": { + "type": "string", + "title": "Source Type" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Tags" + }, + "tool_type": { + "type": "string", + "title": "Tool Type" + }, + "updated_at": { + "type": "string", + "title": "Updated At" + }, + "metadata_": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" + } + }, + "type": "object", + "required": [ + "args_json_schema", + "created_at", + "description", + "json_schema", + "name", + "return_char_limit", + "source_code", + "source_type", + "tags", + "tool_type", + "updated_at" + ], + "title": "ToolSchema" + }, + "letta__server__rest_api__routers__v1__agents__CompactionRequest": { + "properties": { + "compaction_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/CompactionSettings-Input" + }, + { + "type": "null" + } + ], + "description": "Optional compaction settings to use for this summarization request. If not provided, the agent's default settings will be used." + } + }, + "type": "object", + "title": "CompactionRequest" + }, + "letta__server__rest_api__routers__v1__conversations__CompactionRequest": { + "properties": { + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Agent Id", + "description": "Agent ID for agent-direct mode with 'default' conversation. Use with conversation_id='default' in the URL path." + }, + "compaction_settings": { + "anyOf": [ + { + "$ref": "#/components/schemas/CompactionSettings-Input" + }, + { + "type": "null" + } + ], + "description": "Optional compaction settings to use for this summarization request. If not provided, the agent's default settings will be used." + } + }, + "type": "object", + "title": "CompactionRequest" + }, + "letta__server__rest_api__routers__v1__tools__ToolExecuteRequest": { + "properties": { + "args": { + "additionalProperties": true, + "type": "object", + "title": "Args", + "description": "Arguments to pass to the tool" + } + }, + "type": "object", + "title": "ToolExecuteRequest" + }, + "openai__types__chat__chat_completion__ChoiceLogprobs": { + "properties": { + "content": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/openai__types__chat__chat_completion_token_logprob__ChatCompletionTokenLogprob" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Content" + }, + "refusal": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/openai__types__chat__chat_completion_token_logprob__ChatCompletionTokenLogprob" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Refusal" + } + }, + "additionalProperties": true, + "type": "object", + "title": "ChoiceLogprobs", + "description": "Log probability information for the choice." + }, + "openai__types__chat__chat_completion_message_function_tool_call__Function": { + "properties": { + "arguments": { + "type": "string", + "title": "Arguments" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["arguments", "name"], + "title": "Function", + "description": "The function that the model called." + }, + "openai__types__chat__chat_completion_message_function_tool_call_param__Function": { + "properties": { + "arguments": { + "type": "string", + "title": "Arguments" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": ["arguments", "name"], + "title": "Function", + "description": "The function that the model called." + }, + "openai__types__chat__chat_completion_token_logprob__ChatCompletionTokenLogprob": { + "properties": { + "token": { + "type": "string", + "title": "Token" + }, + "bytes": { + "anyOf": [ + { + "items": { + "type": "integer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Bytes" + }, + "logprob": { + "type": "number", + "title": "Logprob" + }, + "top_logprobs": { + "items": { + "$ref": "#/components/schemas/openai__types__chat__chat_completion_token_logprob__TopLogprob" + }, + "type": "array", + "title": "Top Logprobs" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["token", "logprob", "top_logprobs"], + "title": "ChatCompletionTokenLogprob" + }, + "openai__types__chat__chat_completion_token_logprob__TopLogprob": { + "properties": { + "token": { + "type": "string", + "title": "Token" + }, + "bytes": { + "anyOf": [ + { + "items": { + "type": "integer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Bytes" + }, + "logprob": { + "type": "number", + "title": "Logprob" + } + }, + "additionalProperties": true, + "type": "object", + "required": ["token", "logprob"], + "title": "TopLogprob" + }, + "LettaMessageUnion": { + "oneOf": [ + { + "$ref": "#/components/schemas/SystemMessage" + }, + { + "$ref": "#/components/schemas/UserMessage" + }, + { + "$ref": "#/components/schemas/ReasoningMessage" + }, + { + "$ref": "#/components/schemas/HiddenReasoningMessage" + }, + { + "$ref": "#/components/schemas/ToolCallMessage" + }, + { + "$ref": "#/components/schemas/ToolReturnMessage" + }, + { + "$ref": "#/components/schemas/AssistantMessage" + }, + { + "$ref": "#/components/schemas/ApprovalRequestMessage" + }, + { + "$ref": "#/components/schemas/ApprovalResponseMessage" + }, + { + "$ref": "#/components/schemas/SummaryMessage" + }, + { + "$ref": "#/components/schemas/EventMessage" + } + ], + "discriminator": { + "propertyName": "message_type", + "mapping": { + "system_message": "#/components/schemas/SystemMessage", + "user_message": "#/components/schemas/UserMessage", + "reasoning_message": "#/components/schemas/ReasoningMessage", + "hidden_reasoning_message": "#/components/schemas/HiddenReasoningMessage", + "tool_call_message": "#/components/schemas/ToolCallMessage", + "tool_return_message": "#/components/schemas/ToolReturnMessage", + "assistant_message": "#/components/schemas/AssistantMessage", + "approval_request_message": "#/components/schemas/ApprovalRequestMessage", + "approval_response_message": "#/components/schemas/ApprovalResponseMessage", + "summary_message": "#/components/schemas/SummaryMessage", + "event_message": "#/components/schemas/EventMessage" + } + } + }, + "LettaMessageContentUnion": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "$ref": "#/components/schemas/ToolCallContent" + }, + { + "$ref": "#/components/schemas/ToolReturnContent" + }, + { + "$ref": "#/components/schemas/ReasoningContent" + }, + { + "$ref": "#/components/schemas/RedactedReasoningContent" + }, + { + "$ref": "#/components/schemas/OmittedReasoningContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "text": "#/components/schemas/TextContent", + "image": "#/components/schemas/ImageContent", + "tool_call": "#/components/schemas/ToolCallContent", + "tool_return": "#/components/schemas/ToolCallContent", + "reasoning": "#/components/schemas/ReasoningContent", + "redacted_reasoning": "#/components/schemas/RedactedReasoningContent", + "omitted_reasoning": "#/components/schemas/OmittedReasoningContent" + } + } + }, + "LettaAssistantMessageContentUnion": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "text": "#/components/schemas/TextContent" + } + } + }, + "LettaToolReturnContentUnion": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "$ref": "#/components/schemas/ImageContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "text": "#/components/schemas/TextContent", + "image": "#/components/schemas/ImageContent" + } + } + }, + "LettaUserMessageContentUnion": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "$ref": "#/components/schemas/ImageContent" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "text": "#/components/schemas/TextContent", + "image": "#/components/schemas/ImageContent" + } + } + } + }, + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer" + } + } + } +} diff --git a/init.sql b/init.sql new file mode 100644 index 0000000..9d866db --- /dev/null +++ b/init.sql @@ -0,0 +1,36 @@ +-- Title: Init Letta Database + +-- Fetch the docker secrets, if they are available. +-- Otherwise fall back to environment variables, or hardwired 'letta' +\set db_user `([ -r /var/run/secrets/letta-user ] && cat /var/run/secrets/letta-user) || echo "${POSTGRES_USER:-letta}"` +\set db_password `([ -r /var/run/secrets/letta-password ] && cat /var/run/secrets/letta-password) || echo "${POSTGRES_PASSWORD:-letta}"` +\set db_name `([ -r /var/run/secrets/letta-db ] && cat /var/run/secrets/letta-db) || echo "${POSTGRES_DB:-letta}"` + +-- CREATE USER :"db_user" +-- WITH PASSWORD :'db_password' +-- NOCREATEDB +-- NOCREATEROLE +-- ; +-- +-- CREATE DATABASE :"db_name" +-- WITH +-- OWNER = :"db_user" +-- ENCODING = 'UTF8' +-- LC_COLLATE = 'en_US.utf8' +-- LC_CTYPE = 'en_US.utf8' +-- LOCALE_PROVIDER = 'libc' +-- TABLESPACE = pg_default +-- CONNECTION LIMIT = -1; + +-- Set up our schema and extensions in our new database. +\c :"db_name" + +CREATE SCHEMA :"db_name" + AUTHORIZATION :"db_user"; + +ALTER DATABASE :"db_name" + SET search_path TO :"db_name"; + +CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA :"db_name"; + +DROP SCHEMA IF EXISTS public CASCADE; diff --git a/letta/__init__.py b/letta/__init__.py new file mode 100644 index 0000000..24c6008 --- /dev/null +++ b/letta/__init__.py @@ -0,0 +1,47 @@ +import os +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("letta") +except PackageNotFoundError: + # Fallback for development installations + __version__ = "0.16.7" + +if os.environ.get("LETTA_VERSION"): + __version__ = os.environ["LETTA_VERSION"] + +# Import sqlite_functions early to ensure event handlers are registered (only for SQLite) +# This is only needed for the server, not for client usage +try: + from letta.settings import DatabaseChoice, settings + + if settings.database_engine == DatabaseChoice.SQLITE: + from letta.orm import sqlite_functions # noqa: F401 +except ImportError: + # If sqlite_vec is not installed, it's fine for client usage + pass + +# # imports for easier access +from letta.schemas.agent import AgentState as AgentState +from letta.schemas.block import Block as Block +from letta.schemas.embedding_config import EmbeddingConfig as EmbeddingConfig +from letta.schemas.enums import JobStatus as JobStatus +from letta.schemas.file import FileMetadata as FileMetadata +from letta.schemas.job import Job as Job +from letta.schemas.letta_message import LettaErrorMessage as LettaErrorMessage, LettaMessage as LettaMessage, LettaPing as LettaPing +from letta.schemas.letta_stop_reason import LettaStopReason as LettaStopReason +from letta.schemas.llm_config import LLMConfig as LLMConfig +from letta.schemas.memory import ( + ArchivalMemorySummary as ArchivalMemorySummary, + BasicBlockMemory as BasicBlockMemory, + ChatMemory as ChatMemory, + Memory as Memory, + RecallMemorySummary as RecallMemorySummary, +) +from letta.schemas.message import Message as Message +from letta.schemas.organization import Organization as Organization +from letta.schemas.passage import Passage as Passage +from letta.schemas.source import Source as Source +from letta.schemas.tool import Tool as Tool +from letta.schemas.usage import LettaUsageStatistics as LettaUsageStatistics +from letta.schemas.user import User as User diff --git a/letta/adapters/letta_llm_adapter.py b/letta/adapters/letta_llm_adapter.py new file mode 100644 index 0000000..23a41ea --- /dev/null +++ b/letta/adapters/letta_llm_adapter.py @@ -0,0 +1,131 @@ +from abc import ABC, abstractmethod +from typing import AsyncGenerator + +from letta.llm_api.llm_client_base import LLMClientBase +from letta.schemas.enums import LLMCallType +from letta.schemas.letta_message import LettaMessage +from letta.schemas.letta_message_content import ReasoningContent, RedactedReasoningContent, TextContent +from letta.schemas.llm_config import LLMConfig +from letta.schemas.openai.chat_completion_response import ChatCompletionResponse, ChoiceLogprobs, ToolCall +from letta.schemas.provider_trace import BillingContext +from letta.schemas.usage import LettaUsageStatistics +from letta.schemas.user import User +from letta.services.telemetry_manager import TelemetryManager + + +class LettaLLMAdapter(ABC): + """ + Base adapter for handling LLM calls in a unified way. + + This abstract class defines the interface for both blocking and streaming + LLM interactions, allowing the agent to use different execution modes + through a consistent API. + """ + + def __init__( + self, + llm_client: LLMClientBase, + llm_config: LLMConfig, + call_type: LLMCallType, + agent_id: str | None = None, + agent_tags: list[str] | None = None, + run_id: str | None = None, + org_id: str | None = None, + user_id: str | None = None, + billing_context: BillingContext | None = None, + ) -> None: + self.llm_client: LLMClientBase = llm_client + self.llm_config: LLMConfig = llm_config + self.call_type: LLMCallType = call_type + self.agent_id: str | None = agent_id + self.agent_tags: list[str] | None = agent_tags + self.run_id: str | None = run_id + self.org_id: str | None = org_id + self.user_id: str | None = user_id + self.billing_context: BillingContext | None = billing_context + self.message_id: str | None = None + self.request_data: dict | None = None + self.response_data: dict | None = None + self.chat_completions_response: ChatCompletionResponse | None = None + self.reasoning_content: list[TextContent | ReasoningContent | RedactedReasoningContent] | None = None + self.content: list[TextContent | ReasoningContent | RedactedReasoningContent] | None = None + self.tool_call: ToolCall | None = None + self.tool_calls: list[ToolCall] = [] + self.logprobs: ChoiceLogprobs | None = None + # SGLang native endpoint data (for multi-turn RL training) + self.output_ids: list[int] | None = None + self.output_token_logprobs: list[list[float]] | None = None + self.usage: LettaUsageStatistics = LettaUsageStatistics() + self.telemetry_manager: TelemetryManager = TelemetryManager() + self.llm_request_finish_timestamp_ns: int | None = None + self._finish_reason: str | None = None + + @abstractmethod + async def invoke_llm( + self, + request_data: dict, + messages: list, + tools: list, + use_assistant_message: bool, + requires_approval_tools: list[str] = [], + step_id: str | None = None, + actor: User | None = None, + ) -> AsyncGenerator[LettaMessage | None, None]: + """ + Execute the LLM call and yield results as they become available. + + Args: + request_data: The prepared request data for the LLM API + messages: The messages in context for the request + tools: The tools available for the LLM to use + use_assistant_message: If true, use assistant messages when streaming response + requires_approval_tools: The subset of tools that require approval before use + step_id: The step ID associated with this request. If provided, logs request and response data. + actor: The optional actor associated with this request for logging purposes. + + Yields: + LettaMessage: Chunks of data for streaming adapters, or None for blocking adapters + """ + raise NotImplementedError + + @property + def finish_reason(self) -> str | None: + """ + Get the finish_reason from the LLM response. + + Returns: + str | None: The finish_reason if available, None otherwise + """ + if self._finish_reason is not None: + return self._finish_reason + if self.chat_completions_response and self.chat_completions_response.choices: + return self.chat_completions_response.choices[0].finish_reason + return None + + def supports_token_streaming(self) -> bool: + """ + Check if the adapter supports token-level streaming. + + Returns: + bool: True if the adapter can stream back tokens as they are generated, False otherwise + """ + return False + + async def aclose(self) -> None: + """ + Clean up any resources held by the adapter. + + Subclasses that hold long-lived connections (e.g. WebSocket sessions) + should override this to release them. The default implementation is a no-op. + """ + pass + + def log_provider_trace(self, step_id: str | None, actor: User | None) -> None: + """ + Log provider trace data for telemetry purposes. + + Args: + step_id: The step ID associated with this request for logging purposes + actor: The user associated with this request for logging purposes + """ + raise NotImplementedError diff --git a/letta/adapters/letta_llm_request_adapter.py b/letta/adapters/letta_llm_request_adapter.py new file mode 100644 index 0000000..49a06a1 --- /dev/null +++ b/letta/adapters/letta_llm_request_adapter.py @@ -0,0 +1,147 @@ +from typing import AsyncGenerator + +from letta.adapters.letta_llm_adapter import LettaLLMAdapter +from letta.helpers.datetime_helpers import get_utc_timestamp_ns +from letta.otel.tracing import log_attributes, safe_json_dumps, trace_method +from letta.schemas.letta_message import LettaMessage +from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, TextContent +from letta.schemas.provider_trace import ProviderTrace +from letta.schemas.usage import normalize_cache_tokens, normalize_reasoning_tokens +from letta.schemas.user import User +from letta.settings import settings +from letta.utils import safe_create_task + + +class LettaLLMRequestAdapter(LettaLLMAdapter): + """ + Adapter for handling blocking (non-streaming) LLM requests. + + This adapter makes synchronous requests to the LLM and returns complete + responses. It extracts reasoning content, tool calls, and usage statistics + from the response and updates instance variables for access by the agent. + """ + + async def invoke_llm( + self, + request_data: dict, + messages: list, + tools: list, + use_assistant_message: bool, + requires_approval_tools: list[str] = [], + step_id: str | None = None, + actor: str | None = None, + ) -> AsyncGenerator[LettaMessage | None, None]: + """ + Execute a blocking LLM request and yield the response. + + This adapter: + 1. Makes a blocking request to the LLM + 2. Converts the response to chat completion format + 3. Extracts reasoning and tool call information + 4. Updates all instance variables + 5. Yields nothing (blocking mode doesn't stream) + """ + # Store request data + self.request_data = request_data + + # Make the blocking LLM request + self.response_data = await self.llm_client.request_async(request_data, self.llm_config) + self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns() + + # Convert response to chat completion format + self.chat_completions_response = await self.llm_client.convert_response_to_chat_completion( + self.response_data, messages, self.llm_config + ) + + # Extract reasoning content from the response + if self.chat_completions_response.choices[0].message.reasoning_content: + self.reasoning_content = [ + ReasoningContent( + reasoning=self.chat_completions_response.choices[0].message.reasoning_content, + is_native=True, + signature=self.chat_completions_response.choices[0].message.reasoning_content_signature, + ) + ] + elif self.chat_completions_response.choices[0].message.omitted_reasoning_content: + self.reasoning_content = [OmittedReasoningContent()] + elif self.chat_completions_response.choices[0].message.content: + # Reasoning placed into content for legacy reasons + # Carry thought_signature on TextContent when ReasoningContent doesn't exist to hold it + self.reasoning_content = [ + TextContent( + text=self.chat_completions_response.choices[0].message.content, + signature=self.chat_completions_response.choices[0].message.reasoning_content_signature, + ) + ] + else: + # logger.info("No reasoning content found.") + self.reasoning_content = None + + # Extract tool call + if self.chat_completions_response.choices[0].message.tool_calls: + self.tool_call = self.chat_completions_response.choices[0].message.tool_calls[0] + else: + self.tool_call = None + + # Extract logprobs if present + self.logprobs = self.chat_completions_response.choices[0].logprobs + + # Extract usage statistics + self.usage.step_count = 1 + self.usage.completion_tokens = self.chat_completions_response.usage.completion_tokens + self.usage.prompt_tokens = self.chat_completions_response.usage.prompt_tokens + self.usage.total_tokens = self.chat_completions_response.usage.total_tokens + + # Extract cache and reasoning token details using normalized helpers + usage = self.chat_completions_response.usage + self.usage.cached_input_tokens, self.usage.cache_write_tokens = normalize_cache_tokens(usage.prompt_tokens_details) + self.usage.reasoning_tokens = normalize_reasoning_tokens(usage.completion_tokens_details) + + self.log_provider_trace(step_id=step_id, actor=actor) + + yield None + return + + @trace_method + def log_provider_trace(self, step_id: str | None, actor: User | None) -> None: + """ + Log provider trace data for telemetry purposes in a fire-and-forget manner. + + Creates an async task to log the request/response data without blocking + the main execution flow. The task runs in the background. + + Args: + step_id: The step ID associated with this request for logging purposes + actor: The user associated with this request for logging purposes + """ + + if step_id is None or actor is None: + return + + log_attributes( + { + "request_data": safe_json_dumps(self.request_data), + "response_data": safe_json_dumps(self.response_data), + } + ) + + if settings.track_provider_trace: + safe_create_task( + self.telemetry_manager.create_provider_trace_async( + actor=actor, + provider_trace=ProviderTrace( + request_json=self.request_data, + response_json=self.response_data, + step_id=step_id, + agent_id=self.agent_id, + agent_tags=self.agent_tags, + run_id=self.run_id, + call_type=self.call_type, + org_id=self.org_id, + user_id=self.user_id, + llm_config=self.llm_config.model_dump() if self.llm_config else None, + billing_context=self.billing_context, + ), + ), + label="create_provider_trace", + ) diff --git a/letta/adapters/letta_llm_stream_adapter.py b/letta/adapters/letta_llm_stream_adapter.py new file mode 100644 index 0000000..b04763d --- /dev/null +++ b/letta/adapters/letta_llm_stream_adapter.py @@ -0,0 +1,273 @@ +from __future__ import annotations + +import asyncio +from typing import AsyncGenerator + +from letta.adapters.letta_llm_adapter import LettaLLMAdapter +from letta.errors import LLMError +from letta.helpers.datetime_helpers import get_utc_timestamp_ns +from letta.interfaces.anthropic_streaming_interface import AnthropicStreamingInterface +from letta.interfaces.openai_streaming_interface import OpenAIStreamingInterface +from letta.llm_api.llm_client_base import LLMClientBase +from letta.llm_api.openai_ws_session import OpenAIWSSessionManager +from letta.otel.tracing import log_attributes, safe_json_dumps, trace_method +from letta.schemas.enums import LLMCallType, ProviderType +from letta.schemas.letta_message import LettaMessage +from letta.schemas.llm_config import LLMConfig +from letta.schemas.provider_trace import BillingContext, ProviderTrace +from letta.schemas.user import User +from letta.settings import settings +from letta.utils import safe_create_task + + +class LettaLLMStreamAdapter(LettaLLMAdapter): + """ + Adapter for handling streaming LLM requests with immediate token yielding. + + This adapter supports real-time streaming of tokens from the LLM, providing + minimal time-to-first-token (TTFT) latency. It uses specialized streaming + interfaces for different providers (OpenAI, Anthropic) to handle their + specific streaming formats. + """ + + def __init__( + self, + llm_client: LLMClientBase, + llm_config: LLMConfig, + call_type: LLMCallType, + agent_id: str | None = None, + agent_tags: list[str] | None = None, + run_id: str | None = None, + org_id: str | None = None, + user_id: str | None = None, + billing_context: "BillingContext | None" = None, + use_openai_responses_websocket: bool = False, + ) -> None: + super().__init__( + llm_client, + llm_config, + call_type=call_type, + agent_id=agent_id, + agent_tags=agent_tags, + run_id=run_id, + org_id=org_id, + user_id=user_id, + billing_context=billing_context, + ) + self.interface: OpenAIStreamingInterface | AnthropicStreamingInterface | None = None + self.use_openai_responses_websocket: bool = use_openai_responses_websocket + self._ws_session: OpenAIWSSessionManager | None = None # lazy, created on first WS call + + async def _get_or_create_ws_session(self) -> OpenAIWSSessionManager: + """Lazily create and return the WebSocket session for reuse across steps.""" + if self._ws_session is None: + kwargs = await self.llm_client._prepare_client_kwargs_async(self.llm_config) + self._ws_session = OpenAIWSSessionManager(client_kwargs=kwargs) + return self._ws_session + + async def aclose(self) -> None: + """Close the WebSocket session if one was opened.""" + if self._ws_session is not None: + await self._ws_session.aclose() + self._ws_session = None + + async def invoke_llm( + self, + request_data: dict, + messages: list, + tools: list, + use_assistant_message: bool, + requires_approval_tools: list[str] = [], + step_id: str | None = None, + actor: User | None = None, + ) -> AsyncGenerator[LettaMessage, None]: + """ + Execute a streaming LLM request and yield tokens/chunks as they arrive. + + This adapter: + 1. Makes a streaming request to the LLM + 2. Yields chunks immediately for minimal TTFT + 3. Accumulates response data through the streaming interface + 4. Updates all instance variables after streaming completes + """ + # Store request data + self.request_data = request_data + + # Instantiate streaming interface + if self.llm_config.model_endpoint_type in [ProviderType.anthropic, ProviderType.bedrock, ProviderType.minimax]: + self.interface = AnthropicStreamingInterface( + use_assistant_message=use_assistant_message, + put_inner_thoughts_in_kwarg=self.llm_config.put_inner_thoughts_in_kwargs, + requires_approval_tools=requires_approval_tools, + run_id=self.run_id, + step_id=step_id, + ) + elif self.llm_config.model_endpoint_type in [ + ProviderType.openai, + ProviderType.openrouter, + ProviderType.baseten, + ProviderType.fireworks, + ]: + # For non-v1 agents, always use Chat Completions streaming interface + self.interface = OpenAIStreamingInterface( + use_assistant_message=use_assistant_message, + is_openai_proxy=self.llm_config.provider_name == "lmstudio_openai", + put_inner_thoughts_in_kwarg=self.llm_config.put_inner_thoughts_in_kwargs, + messages=messages, + tools=tools, + requires_approval_tools=requires_approval_tools, + run_id=self.run_id, + step_id=step_id, + ) + else: + raise ValueError(f"Streaming not supported for provider {self.llm_config.model_endpoint_type}") + + # Extract optional parameters + # ttft_span = kwargs.get('ttft_span', None) + + request_start_ns = get_utc_timestamp_ns() + + # Start the streaming request (map provider errors to common LLMError types) + try: + stream = await self.llm_client.stream_async(request_data, self.llm_config) + except Exception as e: + self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns() + latency_ms = int((self.llm_request_finish_timestamp_ns - request_start_ns) / 1_000_000) + await self.llm_client.log_provider_trace_async( + request_data=request_data, + response_json=None, + llm_config=self.llm_config, + latency_ms=latency_ms, + error_msg=str(e), + error_type=type(e).__name__, + ) + if isinstance(e, LLMError): + raise + raise self.llm_client.handle_llm_error(e, llm_config=self.llm_config) + + stream_started = True + + try: + # Process the stream and yield chunks immediately for TTFT + # Wrap in error handling to convert provider errors to common LLMError types + try: + async for chunk in self.interface.process(stream): # TODO: add ttft span + # Yield each chunk immediately as it arrives + yield chunk + except BaseException as e: + self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns() + latency_ms = int((self.llm_request_finish_timestamp_ns - request_start_ns) / 1_000_000) + await self.llm_client.log_provider_trace_async( + request_data=request_data, + response_json=None, + llm_config=self.llm_config, + latency_ms=latency_ms, + error_msg=str(e), + error_type=type(e).__name__, + ) + if isinstance(e, asyncio.CancelledError): + raise + if isinstance(e, LLMError): + raise + raise self.llm_client.handle_llm_error(e, llm_config=self.llm_config) + else: + # After streaming completes, extract the accumulated data + self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns() + finally: + if not stream_started: + return + + if self.llm_request_finish_timestamp_ns is None: + self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns() + + # Extract best-effort request results for trace logging + try: + self.tool_call = self.interface.get_tool_call_object() + except Exception: + self.tool_call = None + + try: + self.reasoning_content = self.interface.get_reasoning_content() + except Exception: + self.reasoning_content = [] + + try: + self.usage = self.interface.get_usage_statistics() + except Exception: + pass + self.usage.step_count = 1 + + try: + self.message_id = self.interface.letta_message_id + except Exception: + self.message_id = None + + # Log request and response data + self.log_provider_trace(step_id=step_id, actor=actor) + + def supports_token_streaming(self) -> bool: + return True + + @trace_method + def log_provider_trace(self, step_id: str | None, actor: User | None) -> None: + """ + Log provider trace data for telemetry purposes in a fire-and-forget manner. + + Creates an async task to log the request/response data without blocking + the main execution flow. For streaming adapters, this includes the final + tool call and reasoning content collected during streaming. + + Args: + step_id: The step ID associated with this request for logging purposes + actor: The user associated with this request for logging purposes + """ + if step_id is None or actor is None: + return + + response_json = { + "content": { + "tool_call": self.tool_call.model_dump_json() if self.tool_call else None, + "reasoning": [content.model_dump_json() for content in self.reasoning_content], + }, + "id": self.interface.message_id, + "model": self.interface.model, + "role": "assistant", + # "stop_reason": "", + # "stop_sequence": None, + "type": "message", + "usage": { + "input_tokens": self.usage.prompt_tokens, + "output_tokens": self.usage.completion_tokens, + }, + } + + # Store response data for future reference + self.response_data = response_json + + log_attributes( + { + "request_data": safe_json_dumps(self.request_data), + "response_data": safe_json_dumps(response_json), + } + ) + + if settings.track_provider_trace: + safe_create_task( + self.telemetry_manager.create_provider_trace_async( + actor=actor, + provider_trace=ProviderTrace( + request_json=self.request_data, + response_json=response_json, + step_id=step_id, + agent_id=self.agent_id, + agent_tags=self.agent_tags, + run_id=self.run_id, + call_type=self.call_type, + org_id=self.org_id, + user_id=self.user_id, + llm_config=self.llm_config.model_dump() if self.llm_config else None, + billing_context=self.billing_context, + ), + ), + label="create_provider_trace", + ) diff --git a/letta/adapters/sglang_native_adapter.py b/letta/adapters/sglang_native_adapter.py new file mode 100644 index 0000000..01ce925 --- /dev/null +++ b/letta/adapters/sglang_native_adapter.py @@ -0,0 +1,411 @@ +""" +SGLang Native Adapter for multi-turn RL training. + +Uses SGLang's native /generate endpoint with input_ids (pre-tokenized via HF +apply_chat_template) to get token IDs and per-token logprobs for loss masking. +""" + +import json +import re +import time +import uuid +from typing import Any, AsyncGenerator, Optional + +from letta.adapters.simple_llm_request_adapter import SimpleLLMRequestAdapter +from letta.helpers.datetime_helpers import get_utc_timestamp_ns +from letta.llm_api.sglang_native_client import SGLangNativeClient +from letta.log import get_logger +from letta.schemas.enums import ProviderType +from letta.schemas.letta_message import LettaMessage +from letta.schemas.letta_message_content import TextContent +from letta.schemas.model import ModelSettingsUnion +from letta.schemas.openai.chat_completion_response import ( + ChatCompletionResponse, + ChatCompletionTokenLogprob, + Choice, + ChoiceLogprobs, + FunctionCall, + Message as ChoiceMessage, + ToolCall, + UsageStatistics, +) + +logger = get_logger(__name__) + +# Global tokenizer cache keyed by model name +_tokenizer_cache: dict[str, Any] = {} + + +def _get_tokenizer(model_name: str) -> Any: + """Load and cache HF tokenizer for the given model.""" + if model_name in _tokenizer_cache: + return _tokenizer_cache[model_name] + + from transformers import AutoTokenizer + + logger.info(f"Loading tokenizer for {model_name}") + tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) + _tokenizer_cache[model_name] = tokenizer + return tokenizer + + +def _resolve_tokenizer_path(model_name: str) -> str: + """Resolve model name to a tokenizer-loadable path. + + Handles handles like 'sglang/slime-sglang//local/path' or + 'slime-sglang//local/path' by extracting the local filesystem path. + """ + # Strip leading provider prefixes (e.g. 'sglang/', 'openai-proxy/') + # until we find either a HF repo id or a local path + parts = model_name.split("/") + # Reconstruct: find where the absolute path starts (leading '/') + # e.g. "slime-sglang//opt/..." -> after splitting on '/' gives ['slime-sglang', '', 'opt', ...] + # join from first empty string onward to recover '/opt/...' + for i, part in enumerate(parts): + if part == "" and i > 0: + local_path = "/" + "/".join(parts[i + 1 :]) + if local_path != "/": + return local_path + return model_name + + +def _messages_to_input_ids(model_name: str, messages: list, tools: list) -> list[int]: + """Apply the model's chat template and return token IDs. + + Uses apply_chat_template(tokenize=True) — single step, correct template, + no double tokenization. Raises clearly if tokenizer cannot be loaded. + """ + tokenizer = _get_tokenizer(_resolve_tokenizer_path(model_name)) + + openai_messages = _to_openai_messages(messages) + openai_tools = _to_openai_tools(tools) if tools else None + + result = tokenizer.apply_chat_template( + openai_messages, + tokenize=True, + add_generation_prompt=True, + tools=openai_tools, + return_tensors=None, # plain Python list + ) + # apply_chat_template may return a BatchEncoding (dict-like) or a plain list. + # BatchEncoding doesn't pass isinstance(dict) checks, so use hasattr instead. + if hasattr(result, "input_ids"): + return list(result.input_ids) + if hasattr(result, "__getitem__") and not isinstance(result, (list, tuple)): + return list(result["input_ids"]) + return list(result) + + +def _to_openai_messages(messages: list) -> list[dict]: + """Convert Letta Message objects to OpenAI-style dicts.""" + result = [] + for msg in messages: + if hasattr(msg, "role"): + role = msg.role + content = msg.content or "" + if isinstance(content, list): + parts = [] + for c in content: + text = getattr(c, "text", None) + if text is not None: + parts.append(text) + content = "\n".join(parts) if parts else "" + tool_calls = getattr(msg, "tool_calls", None) + tool_call_id = getattr(msg, "tool_call_id", None) + name = getattr(msg, "name", None) + else: + role = msg.get("role", "user") + content = msg.get("content", "") or "" + tool_calls = msg.get("tool_calls") + tool_call_id = msg.get("tool_call_id") + name = msg.get("name") + + d: dict = {"role": role, "content": content} + + if tool_calls: + parsed_calls = [] + for tc in tool_calls: + if hasattr(tc, "function"): + tc_id = getattr(tc, "id", None) or f"call_{uuid.uuid4().hex[:8]}" + tc_name = tc.function.name + tc_args = tc.function.arguments + else: + tc_id = tc.get("id", f"call_{uuid.uuid4().hex[:8]}") + tc_name = tc.get("function", {}).get("name", "") + tc_args = tc.get("function", {}).get("arguments", "{}") + # GLM chat template expects arguments as dict, not JSON string + if isinstance(tc_args, str): + try: + tc_args = json.loads(tc_args) + except (json.JSONDecodeError, ValueError): + tc_args = {} + parsed_calls.append( + { + "id": tc_id, + "type": "function", + "function": {"name": tc_name, "arguments": tc_args}, + } + ) + d["tool_calls"] = parsed_calls + + if tool_call_id: + d["tool_call_id"] = tool_call_id + if name and role == "tool": + d["name"] = name + + result.append(d) + return result + + +def _to_openai_tools(tools: list) -> list[dict]: + """Convert tool objects to OpenAI-format dicts.""" + result = [] + for tool in tools: + if isinstance(tool, dict): + result.append(tool if "function" in tool else {"type": "function", "function": tool}) + else: + func = getattr(tool, "function", tool) + result.append( + { + "type": "function", + "function": { + "name": getattr(func, "name", ""), + "description": getattr(func, "description", ""), + "parameters": getattr(func, "parameters", {}), + }, + } + ) + return result + + +def _parse_glm47_tool_calls(text: str) -> list[ToolCall]: + """Parse GLM-4.7 XML tool call format inline (no sglang dependency). + + Format: func_namekv... + """ + tool_calls = [] + for inner in re.findall(r"(.*?)", text, re.DOTALL): + inner = inner.strip() + # JSON format fallback + if inner.startswith("{"): + try: + data = json.loads(inner) + args = data.get("arguments", {}) + if isinstance(args, dict): + args = json.dumps(args) + tool_calls.append( + ToolCall( + id=f"call_{uuid.uuid4().hex[:8]}", + type="function", + function=FunctionCall(name=data.get("name", ""), arguments=args), + ) + ) + continue + except json.JSONDecodeError: + pass + # GLM-4.7 XML format: first non-tag text is function name + name_match = re.match(r"^([^<\n]+)", inner) + if not name_match: + continue + func_name = name_match.group(1).strip() + keys = re.findall(r"(.*?)", inner, re.DOTALL) + vals = re.findall(r"(.*?)", inner, re.DOTALL) + tool_calls.append( + ToolCall( + id=f"call_{uuid.uuid4().hex[:8]}", + type="function", + function=FunctionCall(name=func_name, arguments=json.dumps(dict(zip(keys, vals)))), + ) + ) + return tool_calls + + +def _parse_tool_calls(text: str, tools: list | None = None, tool_call_parser: str = "glm47") -> list[ToolCall]: + """Parse tool calls from response text. + + Tries SGLang's FunctionCallParser first; falls back to inline GLM-4.7 parser. + """ + try: + from sglang.srt.function_call.core_types import Function, Tool + from sglang.srt.function_call.function_call_parser import FunctionCallParser + + sglang_tools: list[Tool] = [] + for t in tools or []: + func = t.get("function", t) if isinstance(t, dict) else getattr(t, "function", t) + name = func.get("name", "") if isinstance(func, dict) else getattr(func, "name", "") + desc = func.get("description", "") if isinstance(func, dict) else getattr(func, "description", "") + params = func.get("parameters", {}) if isinstance(func, dict) else getattr(func, "parameters", {}) + sglang_tools.append(Tool(type="function", function=Function(name=name, description=desc, parameters=params))) + + parser = FunctionCallParser(tools=sglang_tools, tool_call_parser=tool_call_parser) + result = parser.detector.detect_and_parse(text, sglang_tools) + tool_calls = [] + for call in result.calls: + arguments = call.parameters if isinstance(call.parameters, str) else json.dumps(call.parameters) + tool_calls.append( + ToolCall( + id=f"call_{uuid.uuid4().hex[:8]}", + type="function", + function=FunctionCall(name=call.name, arguments=arguments), + ) + ) + return tool_calls + except ImportError: + pass + except Exception as e: + logger.warning(f"SGLang tool call parser ({tool_call_parser}) failed: {e}") + + # Inline GLM-4.7 fallback (no sglang required) + return _parse_glm47_tool_calls(text) + + +def _strip_tool_calls(text: str) -> str: + return re.sub(r".*?", "", text, flags=re.DOTALL).strip() + + +class SGLangNativeAdapter(SimpleLLMRequestAdapter): + """ + Adapter using SGLang's native /generate endpoint for multi-turn RL training. + + Flow: + 1. Apply model's chat template via HF tokenizer → input_ids (single tokenization step) + 2. POST input_ids to SGLang /generate → output_ids + output_token_logprobs + 3. Parse tool calls from response text + """ + + def __init__(self, *args, model_settings: ModelSettingsUnion | None = None, **kwargs): + self.model_settings = model_settings + super().__init__(*args, **kwargs) + self._sglang_client: Optional[SGLangNativeClient] = None + + def _get_sglang_client(self) -> SGLangNativeClient: + if self._sglang_client is None: + base_url = (self.llm_config.model_endpoint or "").rstrip("/") + if base_url.endswith("/v1"): + base_url = base_url[:-3] + self._sglang_client = SGLangNativeClient(base_url=base_url, api_key=None) + return self._sglang_client + + async def invoke_llm( + self, + request_data: dict, + messages: list, + tools: list, + use_assistant_message: bool, + requires_approval_tools: list[str] = [], + step_id: str | None = None, + actor: str | None = None, + ) -> AsyncGenerator[LettaMessage | None, None]: + self.request_data = request_data + + sampling_params = { + "temperature": request_data.get("temperature", 0.7), + "max_new_tokens": request_data.get("max_tokens", 4096), + "top_p": request_data.get("top_p", 0.9), + } + + # Tokenize via HF apply_chat_template — correct template, no double tokenization + openai_msgs = _to_openai_messages(messages) + logger.info(f"SGLang native input: {len(openai_msgs)} messages, roles={[m['role'] for m in openai_msgs]}") + if openai_msgs: + first_content = openai_msgs[0].get("content", "") + logger.info(f" first msg content[:200]: {str(first_content)[:200]}") + input_ids = _messages_to_input_ids(self.llm_config.model, messages, tools) + + client = self._get_sglang_client() + response = await client.generate( + input_ids=input_ids, + sampling_params=sampling_params, + return_logprob=True, + ) + + self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns() + self.response_data = response + + self.output_ids = response.get("output_ids") + meta_info = response.get("meta_info", {}) + self.output_token_logprobs = meta_info.get("output_token_logprobs") + + text_response = response.get("text", "") + finish_reason_raw = response.get("meta_info", {}).get("finish_reason", {}) + output_ids_raw = response.get("output_ids", []) + logger.info(f"SGLang raw response[:300]: {repr(text_response[:300])}") + logger.info(f"SGLang finish_reason: {finish_reason_raw}") + logger.info(f"SGLang output_ids[:15]: {output_ids_raw[:15]}") + # Decode output_ids directly to verify what tokens were generated + try: + tok = _get_tokenizer(_resolve_tokenizer_path(self.llm_config.model)) + logger.info(f"SGLang output decoded[:200]: {repr(tok.decode(output_ids_raw[:50]))}") + except Exception as _e: + pass + + tool_call_parser = "qwen25" + if self.model_settings is not None and getattr(self.model_settings, "provider_type", None) == ProviderType.sglang: + tool_call_parser = getattr(self.model_settings, "tool_call_parser", None) or tool_call_parser + parsed_tool_calls = _parse_tool_calls(text_response, tools=tools, tool_call_parser=tool_call_parser) + content_text = _strip_tool_calls(text_response) + + finish_reason_info = meta_info.get("finish_reason", {}) + finish_reason = finish_reason_info.get("type", "stop") if isinstance(finish_reason_info, dict) else "stop" + if parsed_tool_calls: + finish_reason = "tool_calls" + + logprobs_content = None + if self.output_token_logprobs: + logprobs_content = [ + ChatCompletionTokenLogprob( + token=str(lp[1]) if len(lp) > 1 else "0", + logprob=lp[0] if len(lp) > 0 else 0.0, + bytes=None, + top_logprobs=[], + ) + for lp in self.output_token_logprobs + ] + + choice_logprobs = ChoiceLogprobs(content=logprobs_content) if logprobs_content else None + prompt_tokens = meta_info.get("prompt_tokens", 0) + completion_tokens = len(self.output_ids) if self.output_ids else 0 + + self.chat_completions_response = ChatCompletionResponse( + id=meta_info.get("id", "sglang-native"), + created=int(time.time()), + choices=[ + Choice( + finish_reason=finish_reason, + index=0, + message=ChoiceMessage( + role="assistant", + content=content_text or None, + tool_calls=parsed_tool_calls or None, + ), + logprobs=choice_logprobs, + ) + ], + usage=UsageStatistics( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=prompt_tokens + completion_tokens, + ), + ) + + self.content = [TextContent(text=content_text)] if content_text else None + self.reasoning_content = None + self.tool_calls = parsed_tool_calls + self.tool_call = parsed_tool_calls[0] if parsed_tool_calls else None + self.logprobs = choice_logprobs + + self.usage.step_count = 1 + self.usage.completion_tokens = completion_tokens + self.usage.prompt_tokens = prompt_tokens + self.usage.total_tokens = prompt_tokens + completion_tokens + + self.log_provider_trace(step_id=step_id, actor=actor) + + logger.info( + f"SGLang native: {len(self.output_ids or [])} output tokens, " + f"{len(self.output_token_logprobs or [])} logprobs, " + f"{len(parsed_tool_calls)} tool calls" + ) + + yield None + return diff --git a/letta/adapters/simple_llm_request_adapter.py b/letta/adapters/simple_llm_request_adapter.py new file mode 100644 index 0000000..c50a5d7 --- /dev/null +++ b/letta/adapters/simple_llm_request_adapter.py @@ -0,0 +1,121 @@ +from typing import AsyncGenerator + +from letta.adapters.letta_llm_request_adapter import LettaLLMRequestAdapter +from letta.errors import LLMError +from letta.helpers.datetime_helpers import get_utc_timestamp_ns +from letta.schemas.enums import LLMCallType +from letta.schemas.letta_message import LettaMessage +from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, TextContent +from letta.schemas.usage import normalize_cache_tokens, normalize_reasoning_tokens + + +class SimpleLLMRequestAdapter(LettaLLMRequestAdapter): + """Simplifying assumptions: + + - No inner thoughts in kwargs + - No forced tool calls + - Content native as assistant message + """ + + async def invoke_llm( + self, + request_data: dict, + messages: list, + tools: list, + use_assistant_message: bool, + requires_approval_tools: list[str] = [], + step_id: str | None = None, + actor: str | None = None, + ) -> AsyncGenerator[LettaMessage | None, None]: + """ + Execute a blocking LLM request and yield the response. + + This adapter: + 1. Makes a blocking request to the LLM + 2. Converts the response to chat completion format + 3. Extracts reasoning and tool call information + 4. Updates all instance variables + 5. Yields nothing (blocking mode doesn't stream) + """ + # Store request data + self.request_data = request_data + + # Set telemetry context and make the blocking LLM request + self.llm_client.set_telemetry_context( + telemetry_manager=self.telemetry_manager, + step_id=step_id, + agent_id=self.agent_id, + agent_tags=self.agent_tags, + run_id=self.run_id, + call_type=LLMCallType.agent_step, + org_id=self.org_id, + user_id=self.user_id, + llm_config=self.llm_config.model_dump() if self.llm_config else None, + billing_context=self.billing_context, + ) + try: + self.response_data = await self.llm_client.request_async_with_telemetry(request_data, self.llm_config) + except Exception as e: + if isinstance(e, LLMError): + raise + raise self.llm_client.handle_llm_error(e, llm_config=self.llm_config) + + self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns() + + # Convert response to chat completion format + self.chat_completions_response = await self.llm_client.convert_response_to_chat_completion( + self.response_data, messages, self.llm_config + ) + + # Extract reasoning content from the response + if self.chat_completions_response.choices[0].message.reasoning_content: + self.reasoning_content = [ + ReasoningContent( + reasoning=self.chat_completions_response.choices[0].message.reasoning_content, + is_native=True, + signature=self.chat_completions_response.choices[0].message.reasoning_content_signature, + ) + ] + elif self.chat_completions_response.choices[0].message.omitted_reasoning_content: + self.reasoning_content = [OmittedReasoningContent()] + else: + # logger.info("No reasoning content found.") + self.reasoning_content = None + + if self.chat_completions_response.choices[0].message.content: + # NOTE: big difference - 'content' goes into 'content' + # Reasoning placed into content for legacy reasons + # Carry thought_signature on TextContent when ReasoningContent doesn't exist to hold it + # (e.g. Gemini 2.5 Flash with include_thoughts=False still returns thought_signature) + orphan_sig = ( + self.chat_completions_response.choices[0].message.reasoning_content_signature if not self.reasoning_content else None + ) + self.content = [TextContent(text=self.chat_completions_response.choices[0].message.content, signature=orphan_sig)] + else: + self.content = None + + if self.reasoning_content and len(self.reasoning_content) > 0: + # Temp workaround to consolidate parts to persist reasoning content, this should be integrated better + self.content = self.reasoning_content + (self.content or []) + + # Extract tool call + tool_calls = self.chat_completions_response.choices[0].message.tool_calls or [] + self.tool_calls = list(tool_calls) + self.tool_call = self.tool_calls[0] if self.tool_calls else None + + # Extract logprobs if present + self.logprobs = self.chat_completions_response.choices[0].logprobs + + # Extract usage statistics + self.usage.step_count = 1 + self.usage.completion_tokens = self.chat_completions_response.usage.completion_tokens + self.usage.prompt_tokens = self.chat_completions_response.usage.prompt_tokens + self.usage.total_tokens = self.chat_completions_response.usage.total_tokens + + # Extract cache and reasoning token details using normalized helpers + usage = self.chat_completions_response.usage + self.usage.cached_input_tokens, self.usage.cache_write_tokens = normalize_cache_tokens(usage.prompt_tokens_details) + self.usage.reasoning_tokens = normalize_reasoning_tokens(usage.completion_tokens_details) + + yield None + return diff --git a/letta/adapters/simple_llm_stream_adapter.py b/letta/adapters/simple_llm_stream_adapter.py new file mode 100644 index 0000000..b826c96 --- /dev/null +++ b/letta/adapters/simple_llm_stream_adapter.py @@ -0,0 +1,321 @@ +import asyncio +from typing import AsyncGenerator + +from letta.adapters.letta_llm_stream_adapter import LettaLLMStreamAdapter +from letta.errors import LLMError +from letta.log import get_logger + +logger = get_logger(__name__) +from letta.helpers.datetime_helpers import get_utc_timestamp_ns +from letta.interfaces.anthropic_parallel_tool_call_streaming_interface import SimpleAnthropicStreamingInterface +from letta.interfaces.gemini_streaming_interface import SimpleGeminiStreamingInterface +from letta.interfaces.openai_streaming_interface import SimpleOpenAIResponsesStreamingInterface, SimpleOpenAIStreamingInterface +from letta.llm_api.openai_client import OpenAIClient +from letta.otel.tracing import log_attributes, safe_json_dumps, trace_method +from letta.schemas.enums import ProviderType +from letta.schemas.letta_message import LettaMessage +from letta.schemas.provider_trace import ProviderTrace +from letta.schemas.user import User +from letta.server.rest_api.streaming_response import get_cancellation_event_for_run +from letta.settings import settings +from letta.utils import safe_create_task + + +class SimpleLLMStreamAdapter(LettaLLMStreamAdapter): + """ + Adapter for handling streaming LLM requests with immediate token yielding. + + This adapter supports real-time streaming of tokens from the LLM, providing + minimal time-to-first-token (TTFT) latency. It uses specialized streaming + interfaces for different providers (OpenAI, Anthropic) to handle their + specific streaming formats. + """ + + def _extract_tool_calls(self) -> list: + """extract tool calls from interface, trying parallel API first then single API""" + # try multi-call api if available + if hasattr(self.interface, "get_tool_call_objects"): + try: + calls = self.interface.get_tool_call_objects() + if calls: + return calls + except Exception: + pass + + # fallback to single-call api + try: + single = self.interface.get_tool_call_object() + return [single] if single else [] + except Exception: + return [] + + async def invoke_llm( + self, + request_data: dict, + messages: list, + tools: list, + use_assistant_message: bool, # NOTE: not used + requires_approval_tools: list[str] = [], + step_id: str | None = None, + actor: User | None = None, + ) -> AsyncGenerator[LettaMessage, None]: + """ + Execute a streaming LLM request and yield tokens/chunks as they arrive. + + This adapter: + 1. Makes a streaming request to the LLM + 2. Yields chunks immediately for minimal TTFT + 3. Accumulates response data through the streaming interface + 4. Updates all instance variables after streaming completes + """ + # Store request data + self.request_data = request_data + + # Track request start time for latency calculation + request_start_ns = get_utc_timestamp_ns() + + # Get cancellation event for this run to enable graceful cancellation (before branching) + cancellation_event = get_cancellation_event_for_run(self.run_id) if self.run_id else None + + # Instantiate streaming interface + if self.llm_config.model_endpoint_type in [ProviderType.anthropic, ProviderType.bedrock, ProviderType.minimax]: + # NOTE: different + self.interface = SimpleAnthropicStreamingInterface( + requires_approval_tools=requires_approval_tools, + run_id=self.run_id, + step_id=step_id, + llm_config=self.llm_config, + ) + elif self.llm_config.model_endpoint_type in [ + ProviderType.openai, + ProviderType.deepseek, + ProviderType.openrouter, + ProviderType.zai, + ProviderType.zai_coding, + ProviderType.baseten, + ProviderType.fireworks, + ProviderType.chatgpt_oauth, + ]: + # Decide interface based on payload shape + use_responses = "input" in request_data and "messages" not in request_data + # No support for Responses API proxy + is_proxy = self.llm_config.provider_name == "lmstudio_openai" + + # ChatGPT OAuth always uses Responses API format + if self.llm_config.model_endpoint_type == ProviderType.chatgpt_oauth: + use_responses = True + is_proxy = False + + if use_responses and not is_proxy: + self.interface = SimpleOpenAIResponsesStreamingInterface( + is_openai_proxy=False, + messages=messages, + tools=tools, + requires_approval_tools=requires_approval_tools, + run_id=self.run_id, + step_id=step_id, + cancellation_event=cancellation_event, + ) + else: + self.interface = SimpleOpenAIStreamingInterface( + is_openai_proxy=self.llm_config.provider_name == "lmstudio_openai", + messages=messages, + tools=tools, + requires_approval_tools=requires_approval_tools, + model=self.llm_config.model, + run_id=self.run_id, + step_id=step_id, + cancellation_event=cancellation_event, + ) + elif self.llm_config.model_endpoint_type in [ProviderType.google_ai, ProviderType.google_vertex]: + self.interface = SimpleGeminiStreamingInterface( + model=self.llm_config.model, + requires_approval_tools=requires_approval_tools, + run_id=self.run_id, + step_id=step_id, + cancellation_event=cancellation_event, + ) + else: + raise ValueError(f"Streaming not supported for provider {self.llm_config.model_endpoint_type}") + + # Start the streaming request (map provider errors to common LLMError types) + try: + # Gemini uses async generator pattern (no await) to maintain connection lifecycle + # Other providers return awaitables that resolve to iterators + if self.llm_config.model_endpoint_type in [ProviderType.google_ai, ProviderType.google_vertex]: + stream = self.llm_client.stream_async(request_data, self.llm_config) + elif self.use_openai_responses_websocket and isinstance(self.llm_client, OpenAIClient): + ws_session = await self._get_or_create_ws_session() + stream = await self.llm_client.stream_async( + request_data, + self.llm_config, + use_websocket=True, + ws_session=ws_session, + ) + else: + stream = await self.llm_client.stream_async(request_data, self.llm_config) + except Exception as e: + self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns() + latency_ms = int((self.llm_request_finish_timestamp_ns - request_start_ns) / 1_000_000) + await self.llm_client.log_provider_trace_async( + request_data=request_data, + response_json=None, + llm_config=self.llm_config, + latency_ms=latency_ms, + error_msg=str(e), + error_type=type(e).__name__, + ) + if isinstance(e, LLMError): + raise + raise self.llm_client.handle_llm_error(e, llm_config=self.llm_config) + + stream_started = True + + try: + # Process the stream and yield chunks immediately for TTFT + try: + async for chunk in self.interface.process(stream): # TODO: add ttft span + # Yield each chunk immediately as it arrives + yield chunk + except BaseException as e: + self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns() + latency_ms = int((self.llm_request_finish_timestamp_ns - request_start_ns) / 1_000_000) + await self.llm_client.log_provider_trace_async( + request_data=request_data, + response_json=None, + llm_config=self.llm_config, + latency_ms=latency_ms, + error_msg=str(e), + error_type=type(e).__name__, + ) + if isinstance(e, asyncio.CancelledError): + raise + if isinstance(e, LLMError): + raise + raise self.llm_client.handle_llm_error(e, llm_config=self.llm_config) + else: + # After streaming completes, extract the accumulated data + self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns() + finally: + if not stream_started: + return + + if self.llm_request_finish_timestamp_ns is None: + self.llm_request_finish_timestamp_ns = get_utc_timestamp_ns() + + # extract tool calls from interface (supports both single and parallel calls) + try: + self.tool_calls = self._extract_tool_calls() + except Exception: + self.tool_calls = [] + self.tool_call = self.tool_calls[-1] if self.tool_calls else None + + # Extract reasoning content from the interface + # TODO this should probably just be called "content"? + # self.reasoning_content = self.interface.get_reasoning_content() + + # Extract all content parts + try: + self.content = self.interface.get_content() + except Exception: + self.content = [] + + # Extract usage statistics from the interface + # Each interface implements get_usage_statistics() with provider-specific logic + try: + self.usage = self.interface.get_usage_statistics() + except Exception: + pass + self.usage.step_count = 1 + + # Store any additional data from the interface + try: + self.message_id = self.interface.letta_message_id + except Exception: + self.message_id = None + + # Populate finish_reason for downstream continuation logic. + # In Responses streaming, max_output_tokens is expressed via incomplete_details.reason. + if hasattr(self.interface, "final_response") and self.interface.final_response is not None: + resp = self.interface.final_response + incomplete_details = getattr(resp, "incomplete_details", None) + incomplete_reason = getattr(incomplete_details, "reason", None) if incomplete_details else None + if incomplete_reason == "max_output_tokens": + self._finish_reason = "length" + elif incomplete_reason == "content_filter": + self._finish_reason = "content_filter" + elif incomplete_reason is not None: + # Unknown incomplete reason — preserve it as-is for diagnostics + self._finish_reason = incomplete_reason + elif getattr(resp, "status", None) == "completed": + self._finish_reason = "stop" + + # Log request and response data + self.log_provider_trace(step_id=step_id, actor=actor) + + @trace_method + def log_provider_trace(self, step_id: str | None, actor: User | None) -> None: + """ + Log provider trace data for telemetry purposes in a fire-and-forget manner. + + Creates an async task to log the request/response data without blocking + the main execution flow. For streaming adapters, this includes the final + tool call and reasoning content collected during streaming. + + Args: + step_id: The step ID associated with this request for logging purposes + actor: The user associated with this request for logging purposes + """ + if step_id is None or actor is None: + return + + response_json = { + "content": { + "tool_call": self.tool_call.model_dump_json() if self.tool_call else None, + # "reasoning": [content.model_dump_json() for content in self.reasoning_content], + # NOTE: different + # TODO potentially split this into both content and reasoning? + "content": [content.model_dump_json() for content in self.content], + }, + "id": self.interface.message_id, + "model": self.interface.model, + "role": "assistant", + # "stop_reason": "", + # "stop_sequence": None, + "type": "message", + # Use raw_usage if available for transparent provider trace logging, else fallback + "usage": self.interface.raw_usage + if hasattr(self.interface, "raw_usage") and self.interface.raw_usage + else { + "input_tokens": self.usage.prompt_tokens, + "output_tokens": self.usage.completion_tokens, + }, + } + + log_attributes( + { + "request_data": safe_json_dumps(self.request_data), + "response_data": safe_json_dumps(response_json), + } + ) + + if settings.track_provider_trace: + safe_create_task( + self.telemetry_manager.create_provider_trace_async( + actor=actor, + provider_trace=ProviderTrace( + request_json=self.request_data, + response_json=response_json, + step_id=step_id, + agent_id=self.agent_id, + agent_tags=self.agent_tags, + run_id=self.run_id, + call_type=self.call_type, + org_id=self.org_id, + user_id=self.user_id, + llm_config=self.llm_config.model_dump() if self.llm_config else None, + billing_context=self.billing_context, + ), + ), + label="create_provider_trace", + ) diff --git a/letta/agent.py b/letta/agent.py new file mode 100644 index 0000000..0a03942 --- /dev/null +++ b/letta/agent.py @@ -0,0 +1,1758 @@ +import asyncio +import json +import time +import traceback +import warnings +from abc import ABC, abstractmethod +from typing import Dict, List, Optional, Tuple, Union + +from openai.types.beta.function_tool import FunctionTool as OpenAITool + +from letta.agents.helpers import generate_step_id +from letta.constants import ( + CLI_WARNING_PREFIX, + COMPOSIO_ENTITY_ENV_VAR_KEY, + ERROR_MESSAGE_PREFIX, + FIRST_MESSAGE_ATTEMPTS, + FUNC_FAILED_HEARTBEAT_MESSAGE, + LETTA_CORE_TOOL_MODULE_NAME, + LETTA_MULTI_AGENT_TOOL_MODULE_NAME, + LLM_MAX_TOKENS, + READ_ONLY_BLOCK_EDIT_ERROR, + REQ_HEARTBEAT_MESSAGE, + SEND_MESSAGE_TOOL_NAME, +) +from letta.errors import ContextWindowExceededError +from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source +from letta.functions.composio_helpers import execute_composio_action, generate_composio_action_from_func_name +from letta.functions.functions import get_function_from_module +from letta.helpers import ToolRulesSolver +from letta.helpers.composio_helpers import get_composio_api_key +from letta.helpers.datetime_helpers import get_utc_time +from letta.helpers.json_helpers import json_dumps, json_loads +from letta.helpers.message_helper import convert_message_creates_to_messages +from letta.interface import AgentInterface +from letta.llm_api.helpers import calculate_summarizer_cutoff, get_token_counts_for_messages, is_context_overflow_error +from letta.llm_api.llm_api_tools import create +from letta.llm_api.llm_client import LLMClient +from letta.local_llm.constants import INNER_THOUGHTS_KWARG +from letta.local_llm.utils import num_tokens_from_functions, num_tokens_from_messages +from letta.log import get_logger +from letta.memory import summarize_messages +from letta.orm import User +from letta.otel.tracing import log_event, trace_method +from letta.prompts.prompt_generator import PromptGenerator +from letta.schemas.agent import AgentState, AgentStepResponse, UpdateAgent +from letta.schemas.block import BlockUpdate +from letta.schemas.embedding_config import EmbeddingConfig +from letta.schemas.enums import MessageRole, ProviderType, StepStatus, ToolType +from letta.schemas.letta_message_content import ImageContent, TextContent +from letta.schemas.memory import ContextWindowOverview, Memory +from letta.schemas.message import Message, MessageCreate, ToolReturn +from letta.schemas.openai.chat_completion_response import ChatCompletionResponse, Message as ChatCompletionMessage, UsageStatistics +from letta.schemas.response_format import ResponseFormatType +from letta.schemas.tool import Tool +from letta.schemas.tool_execution_result import ToolExecutionResult +from letta.schemas.tool_rule import TerminalToolRule +from letta.schemas.usage import LettaUsageStatistics +from letta.services.agent_manager import AgentManager +from letta.services.block_manager import BlockManager +from letta.services.helpers.agent_manager_helper import check_supports_structured_output +from letta.services.helpers.tool_parser_helper import runtime_override_tool_json_schema +from letta.services.job_manager import JobManager +from letta.services.mcp.base_client import AsyncBaseMCPClient +from letta.services.message_manager import MessageManager +from letta.services.passage_manager import PassageManager +from letta.services.provider_manager import ProviderManager +from letta.services.step_manager import StepManager +from letta.services.telemetry_manager import NoopTelemetryManager, TelemetryManager +from letta.services.tool_executor.tool_execution_sandbox import ToolExecutionSandbox +from letta.services.tool_manager import ToolManager +from letta.settings import model_settings, settings, summarizer_settings +from letta.streaming_interface import StreamingRefreshCLIInterface +from letta.system import get_heartbeat, get_token_limit_warning, package_function_response, package_summarize_message, package_user_message +from letta.utils import count_tokens, get_friendly_error_msg, get_tool_call_id, log_telemetry, parse_json, validate_function_response + +logger = get_logger(__name__) + + +class BaseAgent(ABC): + """ + Abstract class for all agents. + Only one interface is required: step. + """ + + @abstractmethod + def step( + self, + input_messages: List[MessageCreate], + ) -> LettaUsageStatistics: + """ + Top-level event message handler for the agent. + """ + raise NotImplementedError + + +class Agent(BaseAgent): + def __init__( + self, + interface: Optional[Union[AgentInterface, StreamingRefreshCLIInterface]], + agent_state: AgentState, # in-memory representation of the agent state (read from multiple tables) + user: User, + # extras + first_message_verify_mono: bool = True, # TODO move to config? + # MCP sessions, state held in-memory in the server + mcp_clients: Optional[Dict[str, AsyncBaseMCPClient]] = None, + save_last_response: bool = False, + ): + assert isinstance(agent_state.memory, Memory), f"Memory object is not of type Memory: {type(agent_state.memory)}" + # Hold a copy of the state that was used to init the agent + self.agent_state = agent_state + assert isinstance(self.agent_state.memory, Memory), f"Memory object is not of type Memory: {type(self.agent_state.memory)}" + + self.user = user + + # initialize a tool rules solver + self.tool_rules_solver = ToolRulesSolver(tool_rules=agent_state.tool_rules) + + # gpt-4, gpt-3.5-turbo, ... + self.model = self.agent_state.llm_config.model + self.supports_structured_output = check_supports_structured_output(model=self.model, tool_rules=agent_state.tool_rules) + + # if there are tool rules, print out a warning + if not self.supports_structured_output and agent_state.tool_rules: + for rule in agent_state.tool_rules: + if not isinstance(rule, TerminalToolRule): + warnings.warn("Tool rules only work reliably for model backends that support structured outputs (e.g. OpenAI gpt-4o).") + break + + # state managers + self.block_manager = BlockManager() + + # Interface must implement: + # - internal_monologue + # - assistant_message + # - function_message + # ... + # Different interfaces can handle events differently + # e.g., print in CLI vs send a discord message with a discord bot + self.interface = interface + + # Create the persistence manager object based on the AgentState info + self.message_manager = MessageManager() + self.passage_manager = PassageManager() + self.provider_manager = ProviderManager() + self.agent_manager = AgentManager() + self.job_manager = JobManager() + self.step_manager = StepManager() + self.telemetry_manager = TelemetryManager() if settings.llm_api_logging else NoopTelemetryManager() + + # State needed for heartbeat pausing + + self.first_message_verify_mono = first_message_verify_mono + + # Controls if the convo memory pressure warning is triggered + # When an alert is sent in the message queue, set this to True (to avoid repeat alerts) + # When the summarizer is run, set this back to False (to reset) + self.agent_alerted_about_memory_pressure = False + + # Load last function response from message history + self.last_function_response = self.load_last_function_response() + + # Save last responses in memory + self.save_last_response = save_last_response + self.last_response_messages = [] + + # Logger that the Agent specifically can use, will also report the agent_state ID with the logs + self.logger = get_logger(agent_state.id) + + # MCPClient, state/sessions managed by the server + # TODO: This is temporary, as a bridge + self.mcp_clients = None + # TODO: no longer supported + # if mcp_clients: + # self.mcp_clients = {client_id: client.to_sync_client() for client_id, client in mcp_clients.items()} + + def load_last_function_response(self): + """Load the last function response from message history""" + in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_state.id, actor=self.user) + for i in range(len(in_context_messages) - 1, -1, -1): + msg = in_context_messages[i] + if msg.role == MessageRole.tool and msg.content and len(msg.content) == 1 and isinstance(msg.content[0], TextContent): + text_content = msg.content[0].text + try: + response_json = json.loads(text_content) + if response_json.get("message"): + return response_json["message"] + except (json.JSONDecodeError, KeyError): + raise ValueError(f"Invalid JSON format in message: {text_content}") + return None + + def ensure_read_only_block_not_modified(self, new_memory: Memory) -> None: + """ + Throw an error if a read-only block has been modified + """ + for label in self.agent_state.memory.list_block_labels(): + if self.agent_state.memory.get_block(label).read_only: + if new_memory.get_block(label).value != self.agent_state.memory.get_block(label).value: + raise ValueError(READ_ONLY_BLOCK_EDIT_ERROR) + + def update_memory_if_changed(self, new_memory: Memory) -> bool: + """ + Update internal memory object and system prompt if there have been modifications. + + Args: + new_memory (Memory): the new memory object to compare to the current memory object + + Returns: + modified (bool): whether the memory was updated + """ + system_message = self.message_manager.get_message_by_id(message_id=self.agent_state.message_ids[0], actor=self.user) + if new_memory.compile() not in system_message.content[0].text: + # update the blocks (LRW) in the DB + for label in self.agent_state.memory.list_block_labels(): + updated_value = new_memory.get_block(label).value + if updated_value != self.agent_state.memory.get_block(label).value: + # update the block if it's changed + block_id = self.agent_state.memory.get_block(label).id + self.block_manager.update_block(block_id=block_id, block_update=BlockUpdate(value=updated_value), actor=self.user) + + # refresh memory from DB (using block ids) + self.agent_state.memory = Memory( + blocks=[self.block_manager.get_block_by_id(block.id, actor=self.user) for block in self.agent_state.memory.get_blocks()], + file_blocks=self.agent_state.memory.file_blocks, + agent_type=self.agent_state.agent_type, + ) + + # NOTE: don't do this since re-buildin the memory is handled at the start of the step + # rebuild memory - this records the last edited timestamp of the memory + # TODO: pass in update timestamp from block edit time + self.agent_state = self.agent_manager.rebuild_system_prompt(agent_id=self.agent_state.id, actor=self.user) + + return True + + return False + + def _handle_function_error_response( + self, + error_msg: str, + tool_call_id: str, + function_name: str, + function_args: dict, + function_response: str, + messages: List[Message], + tool_returns: Optional[List[ToolReturn]] = None, + include_function_failed_message: bool = False, + group_id: Optional[str] = None, + ) -> List[Message]: + """ + Handle error from function call response + """ + # Update tool rules + self.last_function_response = function_response + self.tool_rules_solver.register_tool_call(function_name) + + # Extend conversation with function response + function_response = package_function_response(False, error_msg, self.agent_state.timezone) + new_message = Message( + agent_id=self.agent_state.id, + # Base info OpenAI-style + model=self.model, + role="tool", + name=function_name, # NOTE: when role is 'tool', the 'name' is the function name, not agent name + content=[TextContent(text=function_response)], + tool_call_id=tool_call_id, + # Letta extras + tool_returns=tool_returns, + group_id=group_id, + ) + messages.append(new_message) + self.interface.function_message(f"Error: {error_msg}", msg_obj=new_message, chunk_index=0) + if include_function_failed_message: + self.interface.function_message(f"Ran {function_name}({function_args})", msg_obj=new_message) + + # Return updated messages + return messages + + def _runtime_override_tool_json_schema( + self, + functions_list: List[Dict | None], + ) -> List[Dict | None]: + """Override the tool JSON schema at runtime for a particular tool if conditions are met.""" + + # Currently just injects `send_message` with a `response_format` if provided to the agent. + if self.agent_state.response_format and self.agent_state.response_format.type != ResponseFormatType.text: + for func in functions_list: + if func["name"] == SEND_MESSAGE_TOOL_NAME: + if self.agent_state.response_format.type == ResponseFormatType.json_schema: + func["parameters"]["properties"]["message"] = self.agent_state.response_format.json_schema["schema"] + if self.agent_state.response_format.type == ResponseFormatType.json_object: + func["parameters"]["properties"]["message"] = { + "type": "object", + "description": "Message contents. All unicode (including emojis) are supported.", + "additionalProperties": True, + "properties": {}, + } + break + return functions_list + + @trace_method + def _get_ai_reply( + self, + message_sequence: List[Message], + function_call: Optional[str] = None, + first_message: bool = False, + stream: bool = False, # TODO move to config? + empty_response_retry_limit: int = 3, + backoff_factor: float = 0.5, # delay multiplier for exponential backoff + max_delay: float = 10.0, # max delay between retries + step_count: Optional[int] = None, + last_function_failed: bool = False, + put_inner_thoughts_first: bool = True, + step_id: Optional[str] = None, + ) -> ChatCompletionResponse | None: + """Get response from LLM API with robust retry mechanism.""" + log_telemetry(self.logger, "_get_ai_reply start") + available_tools = set([t.name for t in self.agent_state.tools]) + agent_state_tool_jsons = [t.json_schema for t in self.agent_state.tools] + + # Get allowed tools or allow all if none are allowed + allowed_tool_names = self.tool_rules_solver.get_allowed_tool_names( + available_tools=available_tools, last_function_response=self.last_function_response + ) or list(available_tools) + + # Don't allow a tool to be called if it failed last time + if last_function_failed and self.tool_rules_solver.tool_call_history: + allowed_tool_names = [f for f in allowed_tool_names if f != self.tool_rules_solver.tool_call_history[-1]] + if not allowed_tool_names: + return None + + allowed_functions = [func for func in agent_state_tool_jsons if func["name"] in allowed_tool_names] + # Extract terminal tool names from tool rules + terminal_tool_names = {rule.tool_name for rule in self.tool_rules_solver.terminal_tool_rules} + allowed_functions = runtime_override_tool_json_schema( + tool_list=allowed_functions, + response_format=self.agent_state.response_format, + request_heartbeat=True, + terminal_tools=terminal_tool_names, + ) + + # For the first message, force the initial tool if one is specified + force_tool_call = None + if ( + step_count is not None + and step_count == 0 + and not self.supports_structured_output + and len(self.tool_rules_solver.init_tool_rules) > 0 + ): + # TODO: This just seems wrong? What if there are more than 1 init tool rules? + force_tool_call = self.tool_rules_solver.init_tool_rules[0].tool_name + # Force a tool call if exactly one tool is specified + elif step_count is not None and step_count > 0 and len(allowed_tool_names) == 1: + force_tool_call = allowed_tool_names[0] + + for attempt in range(1, empty_response_retry_limit + 1): + try: + log_telemetry(self.logger, "_get_ai_reply create start") + # New LLM client flow + llm_client = LLMClient.create( + provider_type=self.agent_state.llm_config.model_endpoint_type, + put_inner_thoughts_first=put_inner_thoughts_first, + actor=self.user, + ) + + if llm_client and not stream: + response = llm_client.send_llm_request( + messages=message_sequence, + llm_config=self.agent_state.llm_config, + tools=allowed_functions, + force_tool_call=force_tool_call, + telemetry_manager=self.telemetry_manager, + step_id=step_id, + ) + else: + # Fallback to existing flow + for message in message_sequence: + if isinstance(message.content, list): + + def get_fallback_text_content(content): + if isinstance(content, ImageContent): + return TextContent(text="[Image Here]") + return content + + message.content = [get_fallback_text_content(content) for content in message.content] + + response = create( + llm_config=self.agent_state.llm_config, + messages=message_sequence, + user_id=self.agent_state.created_by_id, + functions=allowed_functions, + # functions_python=self.functions_python, do we need this? + function_call=function_call, + first_message=first_message, + force_tool_call=force_tool_call, + stream=stream, + stream_interface=self.interface, + put_inner_thoughts_first=put_inner_thoughts_first, + name=self.agent_state.name, + telemetry_manager=self.telemetry_manager, + step_id=step_id, + actor=self.user, + ) + log_telemetry(self.logger, "_get_ai_reply create finish") + + # These bottom two are retryable + if len(response.choices) == 0 or response.choices[0] is None: + raise ValueError(f"API call returned an empty message: {response}") + + if response.choices[0].finish_reason not in ["stop", "function_call", "tool_calls"]: + if response.choices[0].finish_reason == "length": + # This is not retryable, hence RuntimeError v.s. ValueError + raise RuntimeError("Finish reason was length (maximum context length)") + else: + raise ValueError(f"Bad finish reason from API: {response.choices[0].finish_reason}") + log_telemetry(self.logger, "_handle_ai_response finish") + + except ValueError as ve: + if attempt >= empty_response_retry_limit: + warnings.warn(f"Retry limit reached. Final error: {ve}") + log_telemetry(self.logger, "_handle_ai_response finish ValueError") + raise Exception(f"Retries exhausted and no valid response received. Final error: {ve}") + else: + delay = min(backoff_factor * (2 ** (attempt - 1)), max_delay) + warnings.warn(f"Attempt {attempt} failed: {ve}. Retrying in {delay} seconds...") + time.sleep(delay) + continue + + except Exception as e: + # For non-retryable errors, exit immediately + log_telemetry(self.logger, "_handle_ai_response finish generic Exception") + raise e + + # check if we are going over the context window: this allows for articifial constraints + if response.usage.total_tokens > self.agent_state.llm_config.context_window: + # trigger summarization + log_telemetry(self.logger, "_get_ai_reply summarize_messages_inplace") + self.summarize_messages_inplace() + + # return the response + return response + + log_telemetry(self.logger, "_handle_ai_response finish catch-all exception") + raise Exception("Retries exhausted and no valid response received.") + + @trace_method + def _handle_ai_response( + self, + response_message: ChatCompletionMessage, # TODO should we eventually move the Message creation outside of this function? + override_tool_call_id: bool = False, + # If we are streaming, we needed to create a Message ID ahead of time, + # and now we want to use it in the creation of the Message object + # TODO figure out a cleaner way to do this + response_message_id: Optional[str] = None, + group_id: Optional[str] = None, + ) -> Tuple[List[Message], bool, bool]: + """Handles parsing and function execution""" + log_telemetry(self.logger, "_handle_ai_response start") + # Hacky failsafe for now to make sure we didn't implement the streaming Message ID creation incorrectly + if response_message_id is not None: + assert response_message_id.startswith("message-"), response_message_id + + messages = [] # append these to the history when done + function_name = None + function_args = {} + chunk_index = 0 + + # Step 2: check if LLM wanted to call a function + if response_message.function_call or (response_message.tool_calls is not None and len(response_message.tool_calls) > 0): + if response_message.function_call: + raise DeprecationWarning(response_message) + if response_message.tool_calls is not None and len(response_message.tool_calls) > 1: + # raise NotImplementedError(f">1 tool call not supported") + # TODO eventually support sequential tool calling + self.logger.warning(f">1 tool call not supported, using index=0 only\n{response_message.tool_calls}") + response_message.tool_calls = [response_message.tool_calls[0]] + assert response_message.tool_calls is not None and len(response_message.tool_calls) > 0 + + # generate UUID for tool call + if override_tool_call_id or response_message.function_call: + warnings.warn("Overriding the tool call can result in inconsistent tool call IDs during streaming") + tool_call_id = get_tool_call_id() # needs to be a string for JSON + response_message.tool_calls[0].id = tool_call_id + else: + tool_call_id = response_message.tool_calls[0].id + assert tool_call_id is not None # should be defined + + # only necessary to add the tool_call_id to a function call (antipattern) + # response_message_dict = response_message.model_dump() + # response_message_dict["tool_call_id"] = tool_call_id + + # role: assistant (requesting tool call, set tool call ID) + messages.append( + # NOTE: we're recreating the message here + # TODO should probably just overwrite the fields? + Message.dict_to_message( + id=response_message_id, + agent_id=self.agent_state.id, + model=self.model, + openai_message_dict=response_message.model_dump(), + name=self.agent_state.name, + group_id=group_id, + ) + ) # extend conversation with assistant's reply + self.logger.debug(f"Function call message: {messages[-1]}") + + nonnull_content = False + if response_message.content or response_message.reasoning_content or response_message.redacted_reasoning_content: + # The content if then internal monologue, not chat + self.interface.internal_monologue(response_message.content, msg_obj=messages[-1], chunk_index=chunk_index) + chunk_index += 1 + # Flag to avoid printing a duplicate if inner thoughts get popped from the function call + nonnull_content = True + + # Step 3: call the function + # Note: the JSON response may not always be valid; be sure to handle errors + function_call = ( + response_message.function_call if response_message.function_call is not None else response_message.tool_calls[0].function + ) + function_name = function_call.name + self.logger.info(f"Request to call function {function_name} with tool_call_id: {tool_call_id}") + + # Failure case 1: function name is wrong (not in agent_state.tools) + target_letta_tool = None + for t in self.agent_state.tools: + if t.name == function_name: + # This force refreshes the target_letta_tool from the database + # We only do this on name match to confirm that the agent state contains a specific tool with the right name + target_letta_tool = ToolManager().get_tool_by_name(tool_name=function_name, actor=self.user) + break + + if not target_letta_tool: + error_msg = f"No function named {function_name}" + function_response = "None" # more like "never ran?" + messages = self._handle_function_error_response( + error_msg, tool_call_id, function_name, function_args, function_response, messages, group_id=group_id + ) + return messages, False, True # force a heartbeat to allow agent to handle error + + # Failure case 2: function name is OK, but function args are bad JSON + try: + raw_function_args = function_call.arguments + function_args = parse_json(raw_function_args) + if not isinstance(function_args, dict): + raise ValueError(f"Function arguments are not a dictionary: {function_args} (raw={raw_function_args})") + except Exception as e: + print(e) + error_msg = f"Error parsing JSON for function '{function_name}' arguments: {function_call.arguments}" + function_response = "None" # more like "never ran?" + messages = self._handle_function_error_response( + error_msg, tool_call_id, function_name, function_args, function_response, messages, group_id=group_id + ) + return messages, False, True # force a heartbeat to allow agent to handle error + + # Check if inner thoughts is in the function call arguments (possible apparently if you are using Azure) + if INNER_THOUGHTS_KWARG in function_args: + response_message.content = function_args.pop(INNER_THOUGHTS_KWARG) + # The content if then internal monologue, not chat + if response_message.content and not nonnull_content: + self.interface.internal_monologue(response_message.content, msg_obj=messages[-1], chunk_index=chunk_index) + chunk_index += 1 + + # (Still parsing function args) + # Handle requests for immediate heartbeat + heartbeat_request = function_args.pop("request_heartbeat", None) + + # Edge case: heartbeat_request is returned as a stringified boolean, we will attempt to parse: + if isinstance(heartbeat_request, str) and heartbeat_request.lower().strip() == "true": + heartbeat_request = True + + if heartbeat_request is None: + heartbeat_request = False + + if not isinstance(heartbeat_request, bool): + self.logger.warning( + f"{CLI_WARNING_PREFIX}'request_heartbeat' arg parsed was not a bool or None, type={type(heartbeat_request)}, value={heartbeat_request}" + ) + heartbeat_request = False + + # Failure case 3: function failed during execution + # NOTE: the msg_obj associated with the "Running " message is the prior assistant message, not the function/tool role message + # this is because the function/tool role message is only created once the function/tool has executed/returned + + # handle cases where we return a json message + if "message" in function_args: + function_args["message"] = str(function_args.get("message", "")) + self.interface.function_message(f"Running {function_name}({function_args})", msg_obj=messages[-1], chunk_index=chunk_index) + chunk_index = 0 # reset chunk index after assistant message + try: + # handle tool execution (sandbox) and state updates + log_telemetry( + self.logger, "_handle_ai_response execute tool start", function_name=function_name, function_args=function_args + ) + log_event( + "tool_call_initiated", + attributes={ + "function_name": function_name, + "target_letta_tool": target_letta_tool.model_dump(), + **{f"function_args.{k}": v for k, v in function_args.items()}, + }, + ) + + tool_execution_result = self.execute_tool_and_persist_state(function_name, function_args, target_letta_tool) + function_response = tool_execution_result.func_return + + log_event( + "tool_call_ended", + attributes={ + "function_response": function_response, + "tool_execution_result": tool_execution_result.model_dump(), + }, + ) + log_telemetry( + self.logger, "_handle_ai_response execute tool finish", function_name=function_name, function_args=function_args + ) + + if tool_execution_result and tool_execution_result.status == "error": + tool_return = ToolReturn( + status=tool_execution_result.status, stdout=tool_execution_result.stdout, stderr=tool_execution_result.stderr + ) + messages = self._handle_function_error_response( + function_response, + tool_call_id, + function_name, + function_args, + function_response, + messages, + [tool_return], + group_id=group_id, + ) + return messages, False, True # force a heartbeat to allow agent to handle error + + # handle trunction + if function_name in ["conversation_search", "conversation_search_date", "archival_memory_search"]: + # with certain functions we rely on the paging mechanism to handle overflow + truncate = False + else: + # but by default, we add a truncation safeguard to prevent bad functions from + # overflow the agent context window + truncate = True + + # get the function response limit + return_char_limit = target_letta_tool.return_char_limit + function_response_string = validate_function_response( + function_response, return_char_limit=return_char_limit, truncate=truncate + ) + function_args.pop("self", None) + function_response = package_function_response(True, function_response_string, self.agent_state.timezone) + function_failed = False + except Exception as e: + function_args.pop("self", None) + # error_msg = f"Error calling function {function_name} with args {function_args}: {str(e)}" + # Less detailed - don't provide full args, idea is that it should be in recent context so no need (just adds noise) + error_msg = get_friendly_error_msg(function_name=function_name, exception_name=type(e).__name__, exception_message=str(e)) + error_msg_user = f"{error_msg}\n{traceback.format_exc()}" + self.logger.error(error_msg_user) + messages = self._handle_function_error_response( + error_msg, + tool_call_id, + function_name, + function_args, + function_response, + messages, + [ToolReturn(status="error", stderr=[error_msg_user])], + include_function_failed_message=True, + group_id=group_id, + ) + return messages, False, True # force a heartbeat to allow agent to handle error + + # Step 4: check if function response is an error + if function_response_string.startswith(ERROR_MESSAGE_PREFIX): + error_msg = function_response_string + tool_return = ToolReturn( + status=tool_execution_result.status, + stdout=tool_execution_result.stdout, + stderr=tool_execution_result.stderr, + ) + messages = self._handle_function_error_response( + error_msg, + tool_call_id, + function_name, + function_args, + function_response, + messages, + [tool_return], + include_function_failed_message=True, + group_id=group_id, + ) + return messages, False, True # force a heartbeat to allow agent to handle error + + # If no failures happened along the way: ... + # Step 5: send the info on the function call and function response to GPT + tool_return = ToolReturn( + status=tool_execution_result.status, + stdout=tool_execution_result.stdout, + stderr=tool_execution_result.stderr, + ) + messages.append( + Message( + agent_id=self.agent_state.id, + # Base info OpenAI-style + model=self.model, + role="tool", + name=function_name, # NOTE: when role is 'tool', the 'name' is the function name, not agent name + content=[TextContent(text=function_response)], + tool_call_id=tool_call_id, + # Letta extras + tool_returns=[tool_return], + group_id=group_id, + ) + ) # extend conversation with function response + self.interface.function_message(f"Ran {function_name}({function_args})", msg_obj=messages[-1], chunk_index=chunk_index) + self.interface.function_message(f"Success: {function_response_string}", msg_obj=messages[-1], chunk_index=chunk_index) + chunk_index += 1 + self.last_function_response = function_response + + else: + # Standard non-function reply + messages.append( + Message.dict_to_message( + id=response_message_id, + agent_id=self.agent_state.id, + model=self.model, + openai_message_dict=response_message.model_dump(), + name=self.agent_state.name, + group_id=group_id, + ) + ) # extend conversation with assistant's reply + self.interface.internal_monologue(response_message.content, msg_obj=messages[-1], chunk_index=chunk_index) + chunk_index += 1 + heartbeat_request = False + function_failed = False + + # rebuild memory + # TODO: @charles please check this + self.agent_state = self.agent_manager.rebuild_system_prompt(agent_id=self.agent_state.id, actor=self.user) + + # Update ToolRulesSolver state with last called function + self.tool_rules_solver.register_tool_call(function_name) + # Update heartbeat request according to provided tool rules + if self.tool_rules_solver.has_children_tools(function_name): + heartbeat_request = True + elif self.tool_rules_solver.is_terminal_tool(function_name): + heartbeat_request = False + + # if continue tool rule, then must request a heartbeat + # TODO: dont even include heartbeats in the args + if self.tool_rules_solver.is_continue_tool(function_name): + heartbeat_request = True + + log_telemetry(self.logger, "_handle_ai_response finish") + return messages, heartbeat_request, function_failed + + @trace_method + def step( + self, + input_messages: List[MessageCreate], + # additional args + chaining: bool = True, + max_chaining_steps: Optional[int] = None, + put_inner_thoughts_first: bool = True, + **kwargs, + ) -> LettaUsageStatistics: + """Run Agent.step in a loop, handling chaining via heartbeat requests and function failures""" + # Defensively clear the tool rules solver history + # Usually this would be extraneous as Agent loop is re-loaded on every message send + # But just to be safe + self.tool_rules_solver.clear_tool_history() + + # Convert MessageCreate objects to Message objects + next_input_messages = convert_message_creates_to_messages(input_messages, self.agent_state.id, self.agent_state.timezone) + counter = 0 + total_usage = UsageStatistics() + step_count = 0 + function_failed = False + steps_messages = [] + while True: + kwargs["first_message"] = False + kwargs["step_count"] = step_count + kwargs["last_function_failed"] = function_failed + step_response = self.inner_step( + messages=next_input_messages, + put_inner_thoughts_first=put_inner_thoughts_first, + **kwargs, + ) + + heartbeat_request = step_response.heartbeat_request + function_failed = step_response.function_failed + token_warning = step_response.in_context_memory_warning + usage = step_response.usage + steps_messages.append(step_response.messages) + + step_count += 1 + total_usage += usage + counter += 1 + self.interface.step_complete() + + # logger.debug("Saving agent state") + # save updated state + save_agent(self) + + # Chain stops + if not chaining: + self.logger.info("No chaining, stopping after one step") + break + elif max_chaining_steps is not None and counter > max_chaining_steps: + self.logger.info(f"Hit max chaining steps, stopping after {counter} steps") + break + # Chain handlers + elif token_warning and summarizer_settings.send_memory_warning_message: + assert self.agent_state.created_by_id is not None + next_input_messages = [ + Message.dict_to_message( + agent_id=self.agent_state.id, + model=self.model, + openai_message_dict={ + "role": "user", # TODO: change to system? + "content": get_token_limit_warning(), + }, + ), + ] + continue # always chain + elif function_failed: + assert self.agent_state.created_by_id is not None + next_input_messages = [ + Message.dict_to_message( + agent_id=self.agent_state.id, + model=self.model, + openai_message_dict={ + "role": "user", # TODO: change to system? + "content": get_heartbeat(self.agent_state.timezone, FUNC_FAILED_HEARTBEAT_MESSAGE), + }, + ) + ] + continue # always chain + elif heartbeat_request: + assert self.agent_state.created_by_id is not None + next_input_messages = [ + Message.dict_to_message( + agent_id=self.agent_state.id, + model=self.model, + openai_message_dict={ + "role": "user", # TODO: change to system? + "content": get_heartbeat(self.agent_state.timezone, REQ_HEARTBEAT_MESSAGE), + }, + ) + ] + continue # always chain + # Letta no-op / yield + else: + break + + if self.agent_state.message_buffer_autoclear: + self.logger.info("Autoclearing message buffer") + self.agent_state = self.agent_manager.trim_all_in_context_messages_except_system(self.agent_state.id, actor=self.user) + + return LettaUsageStatistics(**total_usage.model_dump(), step_count=step_count, steps_messages=steps_messages) + + def inner_step( + self, + messages: List[Message], + first_message: bool = False, + first_message_retry_limit: int = FIRST_MESSAGE_ATTEMPTS, + skip_verify: bool = False, + stream: bool = False, # TODO move to config? + step_count: Optional[int] = None, + metadata: Optional[dict] = None, + summarize_attempt_count: int = 0, + last_function_failed: bool = False, + put_inner_thoughts_first: bool = True, + ) -> AgentStepResponse: + """Runs a single step in the agent loop (generates at most one LLM call)""" + try: + # Extract job_id from metadata if present + job_id = metadata.get("job_id") if metadata else None + + # Declare step_id for the given step to be used as the step is processing. + step_id = generate_step_id() + + # Step 0: update core memory + # only pulling latest block data if shared memory is being used + current_persisted_memory = Memory( + blocks=[self.block_manager.get_block_by_id(block.id, actor=self.user) for block in self.agent_state.memory.get_blocks()], + file_blocks=self.agent_state.memory.file_blocks, + agent_type=self.agent_state.agent_type, + ) # read blocks from DB + self.update_memory_if_changed(current_persisted_memory) + + # Step 1: add user message + if not all(isinstance(m, Message) for m in messages): + raise ValueError(f"messages should be a list of Message, got {[type(m) for m in messages]}") + + in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_state.id, actor=self.user) + input_message_sequence = in_context_messages + messages + + if ( + len(input_message_sequence) > 1 + and input_message_sequence[-1].role != "user" + and input_message_sequence[-1].group_id is None + ): + self.logger.warning(f"{CLI_WARNING_PREFIX}Attempting to run ChatCompletion without user as the last message in the queue") + + # Step 2: send the conversation and available functions to the LLM + response = self._get_ai_reply( + message_sequence=input_message_sequence, + first_message=first_message, + stream=stream, + step_count=step_count, + last_function_failed=last_function_failed, + put_inner_thoughts_first=put_inner_thoughts_first, + step_id=step_id, + ) + if not response: + # EDGE CASE: Function call failed AND there's no tools left for agent to call -> return early + return AgentStepResponse( + messages=input_message_sequence, + heartbeat_request=False, + function_failed=False, # NOTE: this is different from other function fails. We force to return early + in_context_memory_warning=False, + usage=UsageStatistics(), + ) + + # Step 3: check if LLM wanted to call a function + # (if yes) Step 4: call the function + # (if yes) Step 5: send the info on the function call and function response to LLM + response_message = response.choices[0].message + + response_message.model_copy() # TODO why are we copying here? + all_response_messages, heartbeat_request, function_failed = self._handle_ai_response( + response_message, + # TODO this is kind of hacky, find a better way to handle this + # the only time we set up message creation ahead of time is when streaming is on + response_message_id=response.id if stream else None, + group_id=input_message_sequence[-1].group_id, + ) + + # Step 6: extend the message history + if len(messages) > 0: + all_new_messages = messages + all_response_messages + else: + all_new_messages = all_response_messages + + if self.save_last_response: + self.last_response_messages = all_response_messages + + # Check the memory pressure and potentially issue a memory pressure warning + current_total_tokens = response.usage.total_tokens + active_memory_warning = False + + # We can't do summarize logic properly if context_window is undefined + if self.agent_state.llm_config.context_window is None: + # Fallback if for some reason context_window is missing, just set to the default + print(f"{CLI_WARNING_PREFIX}could not find context_window in config, setting to default {LLM_MAX_TOKENS['DEFAULT']}") + print(f"{self.agent_state}") + self.agent_state.llm_config.context_window = ( + LLM_MAX_TOKENS[self.model] if (self.model is not None and self.model in LLM_MAX_TOKENS) else LLM_MAX_TOKENS["DEFAULT"] + ) + + if current_total_tokens > summarizer_settings.memory_warning_threshold * int(self.agent_state.llm_config.context_window): + logger.warning( + f"{CLI_WARNING_PREFIX}last response total_tokens ({current_total_tokens}) > {summarizer_settings.memory_warning_threshold * int(self.agent_state.llm_config.context_window)}" + ) + + log_event( + name="memory_pressure_warning", + attributes={ + "current_total_tokens": current_total_tokens, + "context_window_limit": self.agent_state.llm_config.context_window, + }, + ) + # Only deliver the alert if we haven't already (this period) + if not self.agent_alerted_about_memory_pressure: + active_memory_warning = True + self.agent_alerted_about_memory_pressure = True # it's up to the outer loop to handle this + + else: + logger.info( + f"last response total_tokens ({current_total_tokens}) < {summarizer_settings.memory_warning_threshold * int(self.agent_state.llm_config.context_window)}" + ) + + # Log step - this must happen before messages are persisted + step = self.step_manager.log_step( + actor=self.user, + agent_id=self.agent_state.id, + provider_name=self.agent_state.llm_config.model_endpoint_type, + provider_category=self.agent_state.llm_config.provider_category or "base", + model=self.agent_state.llm_config.model, + model_endpoint=self.agent_state.llm_config.model_endpoint, + context_window_limit=self.agent_state.llm_config.context_window, + usage=response.usage, + provider_id=self.provider_manager.get_provider_id_from_name( + self.agent_state.llm_config.provider_name, + actor=self.user, + ), + job_id=job_id, + step_id=step_id, + project_id=self.agent_state.project_id, + status=StepStatus.SUCCESS, # Set to SUCCESS since we're logging after successful completion + ) + for message in all_new_messages: + message.step_id = step.id + + # Persisting into Messages + self.agent_state = self.agent_manager.append_to_in_context_messages( + all_new_messages, agent_id=self.agent_state.id, actor=self.user + ) + if job_id: + for message in all_new_messages: + if message.role != "user": + self.job_manager.add_message_to_job( + job_id=job_id, + message_id=message.id, + actor=self.user, + ) + + return AgentStepResponse( + messages=all_new_messages, + heartbeat_request=heartbeat_request, + function_failed=function_failed, + in_context_memory_warning=active_memory_warning, + usage=response.usage, + ) + + except Exception as e: + logger.error(f"step() failed\nmessages = {messages}\nerror = {e}") + + # If we got a context alert, try trimming the messages length, then try again + if is_context_overflow_error(e): + in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_state.id, actor=self.user) + + # TODO: this is a patch to resolve immediate issues, should be removed once the summarizer is fixes + if self.agent_state.message_buffer_autoclear: + # no calling the summarizer in this case + logger.error( + f"step() failed with an exception that looks like a context window overflow, but message buffer is set to autoclear, so skipping: '{str(e)}'" + ) + raise e + + if summarize_attempt_count <= summarizer_settings.max_summarizer_retries: + logger.warning( + f"context window exceeded with limit {self.agent_state.llm_config.context_window}, attempting to summarize ({summarize_attempt_count}/{summarizer_settings.max_summarizer_retries}" + ) + # A separate API call to run a summarizer + self.summarize_messages_inplace() + + # Try step again + return self.inner_step( + messages=messages, + first_message=first_message, + first_message_retry_limit=first_message_retry_limit, + skip_verify=skip_verify, + stream=stream, + metadata=metadata, + summarize_attempt_count=summarize_attempt_count + 1, + ) + else: + err_msg = f"Ran summarizer {summarize_attempt_count - 1} times for agent id={self.agent_state.id}, but messages are still overflowing the context window." + token_counts = (get_token_counts_for_messages(in_context_messages),) + logger.error(err_msg) + logger.error(f"num_in_context_messages: {len(self.agent_state.message_ids)}") + logger.error(f"token_counts: {token_counts}") + raise ContextWindowExceededError( + err_msg, + details={ + "num_in_context_messages": len(self.agent_state.message_ids), + "in_context_messages_text": [m.content for m in in_context_messages], + "token_counts": token_counts, + }, + ) + + else: + logger.error(f"step() failed with an unrecognized exception: '{str(e)}'") + traceback.print_exc() + raise e + + def step_user_message(self, user_message_str: str, **kwargs) -> AgentStepResponse: + """Takes a basic user message string, turns it into a stringified JSON with extra metadata, then sends it to the agent + + Example: + -> user_message_str = 'hi' + -> {'message': 'hi', 'type': 'user_message', ...} + -> json.dumps(...) + -> agent.step(messages=[Message(role='user', text=...)]) + """ + # Wrap with metadata, dumps to JSON + assert user_message_str and isinstance(user_message_str, str), ( + f"user_message_str should be a non-empty string, got {type(user_message_str)}" + ) + user_message_json_str = package_user_message(user_message_str, self.agent_state.timezone) + + # Validate JSON via save/load + user_message = validate_json(user_message_json_str) + cleaned_user_message_text, name = strip_name_field_from_user_message(user_message) + + # Turn into a dict + openai_message_dict = {"role": "user", "content": cleaned_user_message_text, "name": name} + + # Create the associated Message object (in the database) + assert self.agent_state.created_by_id is not None, "User ID is not set" + user_message = Message.dict_to_message( + agent_id=self.agent_state.id, + model=self.model, + openai_message_dict=openai_message_dict, + # created_at=timestamp, + ) + + return self.inner_step(messages=[user_message], **kwargs) + + def summarize_messages_inplace(self): + in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_state.id, actor=self.user) + in_context_messages_openai = Message.to_openai_dicts_from_list(in_context_messages) + in_context_messages_openai_no_system = in_context_messages_openai[1:] + token_counts = get_token_counts_for_messages(in_context_messages) + logger.info(f"System message token count={token_counts[0]}") + logger.info(f"token_counts_no_system={token_counts[1:]}") + + if in_context_messages_openai[0]["role"] != "system": + raise RuntimeError(f"in_context_messages_openai[0] should be system (instead got {in_context_messages_openai[0]})") + + # If at this point there's nothing to summarize, throw an error + if len(in_context_messages_openai_no_system) == 0: + raise ContextWindowExceededError( + "Not enough messages to compress for summarization", + details={ + "num_candidate_messages": len(in_context_messages_openai_no_system), + "num_total_messages": len(in_context_messages_openai), + }, + ) + + cutoff = calculate_summarizer_cutoff(in_context_messages=in_context_messages, token_counts=token_counts, logger=logger) + message_sequence_to_summarize = in_context_messages[1:cutoff] # do NOT get rid of the system message + logger.info(f"Attempting to summarize {len(message_sequence_to_summarize)} messages of {len(in_context_messages)}") + + # We can't do summarize logic properly if context_window is undefined + if self.agent_state.llm_config.context_window is None: + # Fallback if for some reason context_window is missing, just set to the default + logger.warning(f"{CLI_WARNING_PREFIX}could not find context_window in config, setting to default {LLM_MAX_TOKENS['DEFAULT']}") + self.agent_state.llm_config.context_window = ( + LLM_MAX_TOKENS[self.model] if (self.model is not None and self.model in LLM_MAX_TOKENS) else LLM_MAX_TOKENS["DEFAULT"] + ) + + summary = summarize_messages( + agent_state=self.agent_state, message_sequence_to_summarize=message_sequence_to_summarize, actor=self.user + ) + logger.info(f"Got summary: {summary}") + + # Metadata that's useful for the agent to see + all_time_message_count = self.message_manager.size(agent_id=self.agent_state.id, actor=self.user) + remaining_message_count = 1 + len(in_context_messages) - cutoff # System + remaining + hidden_message_count = all_time_message_count - remaining_message_count + summary_message_count = len(message_sequence_to_summarize) + summary_message = package_summarize_message( + summary, summary_message_count, hidden_message_count, all_time_message_count, self.agent_state.timezone + ) + logger.info(f"Packaged into message: {summary_message}") + + prior_len = len(in_context_messages_openai) + self.agent_state = self.agent_manager.trim_older_in_context_messages(num=cutoff, agent_id=self.agent_state.id, actor=self.user) + packed_summary_message = {"role": "user", "content": summary_message} + # Prepend the summary + self.agent_state = self.agent_manager.prepend_to_in_context_messages( + messages=[ + Message.dict_to_message( + agent_id=self.agent_state.id, + model=self.model, + openai_message_dict=packed_summary_message, + ) + ], + agent_id=self.agent_state.id, + actor=self.user, + ) + + # reset alert + self.agent_alerted_about_memory_pressure = False + curr_in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_state.id, actor=self.user) + + current_token_count = sum(get_token_counts_for_messages(curr_in_context_messages)) + logger.info(f"Ran summarizer, messages length {prior_len} -> {len(curr_in_context_messages)}") + logger.info(f"Summarizer brought down total token count from {sum(token_counts)} -> {current_token_count}") + log_event( + name="summarization", + attributes={ + "prior_length": prior_len, + "current_length": len(curr_in_context_messages), + "prior_token_count": sum(token_counts), + "current_token_count": current_token_count, + "context_window_limit": self.agent_state.llm_config.context_window, + }, + ) + + def add_function(self, function_name: str) -> str: + # TODO: refactor + raise NotImplementedError + + def remove_function(self, function_name: str) -> str: + # TODO: refactor + raise NotImplementedError + + def migrate_embedding(self, embedding_config: EmbeddingConfig): + """Migrate the agent to a new embedding""" + # TODO: archival memory + + # TODO: recall memory + raise NotImplementedError() + + def get_context_window(self) -> ContextWindowOverview: + """Get the context window of the agent""" + + system_prompt = self.agent_state.system # TODO is this the current system or the initial system? + num_tokens_system = count_tokens(system_prompt) + core_memory = self.agent_state.memory.compile() + num_tokens_core_memory = count_tokens(core_memory) + + # Grab the in-context messages + # conversion of messages to OpenAI dict format, which is passed to the token counter + in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_state.id, actor=self.user) + in_context_messages_openai = Message.to_openai_dicts_from_list(in_context_messages) + + # Check if there's a summary message in the message queue + if ( + len(in_context_messages) > 1 + and in_context_messages[1].role == MessageRole.user + and in_context_messages[1].content + and len(in_context_messages[1].content) == 1 + and isinstance(in_context_messages[1].content[0], TextContent) + # TODO remove hardcoding + and "The following is a summary of the previous " in in_context_messages[1].content[0].text + ): + # Summary message exists + text_content = in_context_messages[1].content[0].text + assert text_content is not None + summary_memory = text_content + num_tokens_summary_memory = count_tokens(text_content) + # with a summary message, the real messages start at index 2 + num_tokens_messages = ( + num_tokens_from_messages(messages=in_context_messages_openai[2:], model=self.model) + if len(in_context_messages_openai) > 2 + else 0 + ) + + else: + summary_memory = None + num_tokens_summary_memory = 0 + # with no summary message, the real messages start at index 1 + num_tokens_messages = ( + num_tokens_from_messages(messages=in_context_messages_openai[1:], model=self.model) + if len(in_context_messages_openai) > 1 + else 0 + ) + + agent_manager_passage_size = self.agent_manager.passage_size(actor=self.user, agent_id=self.agent_state.id) + message_manager_size = self.message_manager.size(actor=self.user, agent_id=self.agent_state.id) + external_memory_summary = PromptGenerator.compile_memory_metadata_block( + memory_edit_timestamp=get_utc_time(), + timezone=self.agent_state.timezone, + previous_message_count=self.message_manager.size(actor=self.user, agent_id=self.agent_state.id), + archival_memory_size=self.agent_manager.passage_size(actor=self.user, agent_id=self.agent_state.id), + ) + num_tokens_external_memory_summary = count_tokens(external_memory_summary) + + # tokens taken up by function definitions + agent_state_tool_jsons = [t.json_schema for t in self.agent_state.tools] + if agent_state_tool_jsons: + available_functions_definitions = [OpenAITool(type="function", function=f) for f in agent_state_tool_jsons] + num_tokens_available_functions_definitions = num_tokens_from_functions(functions=agent_state_tool_jsons, model=self.model) + else: + available_functions_definitions = [] + num_tokens_available_functions_definitions = 0 + + num_tokens_used_total = ( + num_tokens_system # system prompt + + num_tokens_available_functions_definitions # function definitions + + num_tokens_core_memory # core memory + + num_tokens_external_memory_summary # metadata (statistics) about recall/archival + + num_tokens_summary_memory # summary of ongoing conversation + + num_tokens_messages # tokens taken by messages + ) + assert isinstance(num_tokens_used_total, int) + + return ContextWindowOverview( + # context window breakdown (in messages) + num_messages=len(in_context_messages), + num_archival_memory=agent_manager_passage_size, + num_recall_memory=message_manager_size, + num_tokens_external_memory_summary=num_tokens_external_memory_summary, + external_memory_summary=external_memory_summary, + # top-level information + context_window_size_max=self.agent_state.llm_config.context_window, + context_window_size_current=num_tokens_used_total, + # context window breakdown (in tokens) + num_tokens_system=num_tokens_system, + system_prompt=system_prompt, + num_tokens_core_memory=num_tokens_core_memory, + core_memory=core_memory, + num_tokens_summary_memory=num_tokens_summary_memory, + summary_memory=summary_memory, + num_tokens_messages=num_tokens_messages, + messages=in_context_messages, + # related to functions + num_tokens_functions_definitions=num_tokens_available_functions_definitions, + functions_definitions=available_functions_definitions, + ) + + async def get_context_window_async(self) -> ContextWindowOverview: + if settings.environment == "PRODUCTION" and model_settings.anthropic_api_key: + return await self.get_context_window_from_anthropic_async() + return await self.get_context_window_from_tiktoken_async() + + async def get_context_window_from_tiktoken_async(self) -> ContextWindowOverview: + """Get the context window of the agent""" + # Grab the in-context messages + in_context_messages = await self.message_manager.get_messages_by_ids_async( + message_ids=self.agent_state.message_ids, actor=self.user + ) + + # conversion of messages to OpenAI dict format, which is passed to the token counter + in_context_messages_openai = Message.to_openai_dicts_from_list(in_context_messages) + + # Extract system, memory and external summary + if ( + len(in_context_messages) > 0 + and in_context_messages[0].role == MessageRole.system + and in_context_messages[0].content + and len(in_context_messages[0].content) == 1 + and isinstance(in_context_messages[0].content[0], TextContent) + ): + system_message = in_context_messages[0].content[0].text + + external_memory_marker_pos = system_message.find("###") + core_memory_marker_pos = system_message.find("<", external_memory_marker_pos) + if external_memory_marker_pos != -1 and core_memory_marker_pos != -1: + system_prompt = system_message[:external_memory_marker_pos].strip() + external_memory_summary = system_message[external_memory_marker_pos:core_memory_marker_pos].strip() + core_memory = system_message[core_memory_marker_pos:].strip() + else: + # if no markers found, put everything in system message + self.logger.info("No markers found in system message, core_memory and external_memory_summary will not be loaded") + system_prompt = system_message + external_memory_summary = "" + core_memory = "" + else: + # if no system message, fall back on agent's system prompt + self.logger.info("No system message found in history, core_memory and external_memory_summary will not be loaded") + system_prompt = self.agent_state.system + external_memory_summary = "" + core_memory = "" + + num_tokens_system = count_tokens(system_prompt) + num_tokens_core_memory = count_tokens(core_memory) + num_tokens_external_memory_summary = count_tokens(external_memory_summary) + + # Check if there's a summary message in the message queue + if ( + len(in_context_messages) > 1 + and in_context_messages[1].role == MessageRole.user + and in_context_messages[1].content + and len(in_context_messages[1].content) == 1 + and isinstance(in_context_messages[1].content[0], TextContent) + # TODO remove hardcoding + and "The following is a summary of the previous " in in_context_messages[1].content[0].text + ): + # Summary message exists + text_content = in_context_messages[1].content[0].text + assert text_content is not None + summary_memory = text_content + num_tokens_summary_memory = count_tokens(text_content) + # with a summary message, the real messages start at index 2 + num_tokens_messages = ( + num_tokens_from_messages(messages=in_context_messages_openai[2:], model=self.model) + if len(in_context_messages_openai) > 2 + else 0 + ) + + else: + summary_memory = None + num_tokens_summary_memory = 0 + # with no summary message, the real messages start at index 1 + num_tokens_messages = ( + num_tokens_from_messages(messages=in_context_messages_openai[1:], model=self.model) + if len(in_context_messages_openai) > 1 + else 0 + ) + + # tokens taken up by function definitions + agent_state_tool_jsons = [t.json_schema for t in self.agent_state.tools] + if agent_state_tool_jsons: + available_functions_definitions = [OpenAITool(type="function", function=f) for f in agent_state_tool_jsons] + num_tokens_available_functions_definitions = num_tokens_from_functions(functions=agent_state_tool_jsons, model=self.model) + else: + available_functions_definitions = [] + num_tokens_available_functions_definitions = 0 + + num_tokens_used_total = ( + num_tokens_system # system prompt + + num_tokens_available_functions_definitions # function definitions + + num_tokens_core_memory # core memory + + num_tokens_external_memory_summary # metadata (statistics) about recall/archival + + num_tokens_summary_memory # summary of ongoing conversation + + num_tokens_messages # tokens taken by messages + ) + assert isinstance(num_tokens_used_total, int) + + passage_manager_size = await self.passage_manager.agent_passage_size_async( + agent_id=self.agent_state.id, + actor=self.user, + ) + message_manager_size = await self.message_manager.size_async( + agent_id=self.agent_state.id, + actor=self.user, + ) + + return ContextWindowOverview( + # context window breakdown (in messages) + num_messages=len(in_context_messages), + num_archival_memory=passage_manager_size, + num_recall_memory=message_manager_size, + num_tokens_external_memory_summary=num_tokens_external_memory_summary, + external_memory_summary=external_memory_summary, + # top-level information + context_window_size_max=self.agent_state.llm_config.context_window, + context_window_size_current=num_tokens_used_total, + # context window breakdown (in tokens) + num_tokens_system=num_tokens_system, + system_prompt=system_prompt, + num_tokens_core_memory=num_tokens_core_memory, + core_memory=core_memory, + num_tokens_summary_memory=num_tokens_summary_memory, + summary_memory=summary_memory, + num_tokens_messages=num_tokens_messages, + messages=in_context_messages, + # related to functions + num_tokens_functions_definitions=num_tokens_available_functions_definitions, + functions_definitions=available_functions_definitions, + ) + + async def get_context_window_from_anthropic_async(self) -> ContextWindowOverview: + """Get the context window of the agent""" + anthropic_client = LLMClient.create(provider_type=ProviderType.anthropic, actor=self.user) + model = self.agent_state.llm_config.model if self.agent_state.llm_config.model_endpoint_type == "anthropic" else None + + # Grab the in-context messages + in_context_messages = await self.message_manager.get_messages_by_ids_async( + message_ids=self.agent_state.message_ids, actor=self.user + ) + + # conversion of messages to anthropic dict format, which is passed to the token counter + in_context_messages_anthropic = Message.to_anthropic_dicts_from_list(in_context_messages) + + # Extract system, memory and external summary + if ( + len(in_context_messages) > 0 + and in_context_messages[0].role == MessageRole.system + and in_context_messages[0].content + and len(in_context_messages[0].content) == 1 + and isinstance(in_context_messages[0].content[0], TextContent) + ): + system_message = in_context_messages[0].content[0].text + + external_memory_marker_pos = system_message.find("###") + core_memory_marker_pos = system_message.find("<", external_memory_marker_pos) + if external_memory_marker_pos != -1 and core_memory_marker_pos != -1: + system_prompt = system_message[:external_memory_marker_pos].strip() + external_memory_summary = system_message[external_memory_marker_pos:core_memory_marker_pos].strip() + core_memory = system_message[core_memory_marker_pos:].strip() + else: + # if no markers found, put everything in system message + self.logger.info("No markers found in system message, core_memory and external_memory_summary will not be loaded") + system_prompt = system_message + external_memory_summary = "" + core_memory = "" + else: + # if no system message, fall back on agent's system prompt + self.logger.info("No system message found in history, core_memory and external_memory_summary will not be loaded") + system_prompt = self.agent_state.system + external_memory_summary = "" + core_memory = "" + + num_tokens_system_coroutine = anthropic_client.count_tokens(model=model, messages=[{"role": "user", "content": system_prompt}]) + num_tokens_core_memory_coroutine = ( + anthropic_client.count_tokens(model=model, messages=[{"role": "user", "content": core_memory}]) + if core_memory + else asyncio.sleep(0, result=0) + ) + num_tokens_external_memory_summary_coroutine = ( + anthropic_client.count_tokens(model=model, messages=[{"role": "user", "content": external_memory_summary}]) + if external_memory_summary + else asyncio.sleep(0, result=0) + ) + + # Check if there's a summary message in the message queue + if ( + len(in_context_messages) > 1 + and in_context_messages[1].role == MessageRole.user + and in_context_messages[1].content + and len(in_context_messages[1].content) == 1 + and isinstance(in_context_messages[1].content[0], TextContent) + # TODO remove hardcoding + and "The following is a summary of the previous " in in_context_messages[1].content[0].text + ): + # Summary message exists + text_content = in_context_messages[1].content[0].text + assert text_content is not None + summary_memory = text_content + num_tokens_summary_memory_coroutine = anthropic_client.count_tokens( + model=model, messages=[{"role": "user", "content": summary_memory}] + ) + # with a summary message, the real messages start at index 2 + num_tokens_messages_coroutine = ( + anthropic_client.count_tokens(model=model, messages=in_context_messages_anthropic[2:]) + if len(in_context_messages_anthropic) > 2 + else asyncio.sleep(0, result=0) + ) + + else: + summary_memory = None + num_tokens_summary_memory_coroutine = asyncio.sleep(0, result=0) + # with no summary message, the real messages start at index 1 + num_tokens_messages_coroutine = ( + anthropic_client.count_tokens(model=model, messages=in_context_messages_anthropic[1:]) + if len(in_context_messages_anthropic) > 1 + else asyncio.sleep(0, result=0) + ) + + # tokens taken up by function definitions + if self.agent_state.tools and len(self.agent_state.tools) > 0: + available_functions_definitions = [OpenAITool(type="function", function=f.json_schema) for f in self.agent_state.tools] + num_tokens_available_functions_definitions_coroutine = anthropic_client.count_tokens( + model=model, + tools=available_functions_definitions, + ) + else: + available_functions_definitions = [] + num_tokens_available_functions_definitions_coroutine = asyncio.sleep(0, result=0) + + ( + num_tokens_system, + num_tokens_core_memory, + num_tokens_external_memory_summary, + num_tokens_summary_memory, + num_tokens_messages, + num_tokens_available_functions_definitions, + ) = await asyncio.gather( + num_tokens_system_coroutine, + num_tokens_core_memory_coroutine, + num_tokens_external_memory_summary_coroutine, + num_tokens_summary_memory_coroutine, + num_tokens_messages_coroutine, + num_tokens_available_functions_definitions_coroutine, + ) + + num_tokens_used_total = ( + num_tokens_system # system prompt + + num_tokens_available_functions_definitions # function definitions + + num_tokens_core_memory # core memory + + num_tokens_external_memory_summary # metadata (statistics) about recall/archival + + num_tokens_summary_memory # summary of ongoing conversation + + num_tokens_messages # tokens taken by messages + ) + assert isinstance(num_tokens_used_total, int) + + passage_manager_size = await self.passage_manager.agent_passage_size_async( + agent_id=self.agent_state.id, + actor=self.user, + ) + message_manager_size = await self.message_manager.size_async( + agent_id=self.agent_state.id, + actor=self.user, + ) + + return ContextWindowOverview( + # context window breakdown (in messages) + num_messages=len(in_context_messages), + num_archival_memory=passage_manager_size, + num_recall_memory=message_manager_size, + num_tokens_external_memory_summary=num_tokens_external_memory_summary, + external_memory_summary=external_memory_summary, + # top-level information + context_window_size_max=self.agent_state.llm_config.context_window, + context_window_size_current=num_tokens_used_total, + # context window breakdown (in tokens) + num_tokens_system=num_tokens_system, + system_prompt=system_prompt, + num_tokens_core_memory=num_tokens_core_memory, + core_memory=core_memory, + num_tokens_summary_memory=num_tokens_summary_memory, + summary_memory=summary_memory, + num_tokens_messages=num_tokens_messages, + messages=in_context_messages, + # related to functions + num_tokens_functions_definitions=num_tokens_available_functions_definitions, + functions_definitions=available_functions_definitions, + ) + + def count_tokens(self) -> int: + """Count the tokens in the current context window""" + context_window_breakdown = self.get_context_window() + return context_window_breakdown.context_window_size_current + + # TODO: Refactor into separate class v.s. large if/elses here + def execute_tool_and_persist_state(self, function_name: str, function_args: dict, target_letta_tool: Tool) -> ToolExecutionResult: + """ + Execute tool modifications and persist the state of the agent. + Note: only some agent state modifications will be persisted, such as data in the AgentState ORM and block data + """ + # TODO: add agent manager here + orig_memory_str = self.agent_state.memory.compile() + + # TODO: need to have an AgentState object that actually has full access to the block data + # this is because the sandbox tools need to be able to access block.value to edit this data + try: + if target_letta_tool.tool_type == ToolType.LETTA_CORE: + # base tools are allowed to access the `Agent` object and run on the database + callable_func = get_function_from_module(LETTA_CORE_TOOL_MODULE_NAME, function_name) + function_args["self"] = self # need to attach self to arg since it's dynamically linked + function_response = callable_func(**function_args) + elif target_letta_tool.tool_type == ToolType.LETTA_MULTI_AGENT_CORE: + callable_func = get_function_from_module(LETTA_MULTI_AGENT_TOOL_MODULE_NAME, function_name) + function_args["self"] = self # need to attach self to arg since it's dynamically linked + function_response = callable_func(**function_args) + elif target_letta_tool.tool_type == ToolType.LETTA_MEMORY_CORE or target_letta_tool.tool_type == ToolType.LETTA_SLEEPTIME_CORE: + callable_func = get_function_from_module(LETTA_CORE_TOOL_MODULE_NAME, function_name) + agent_state_copy = self.agent_state.__deepcopy__() + function_args["agent_state"] = agent_state_copy # need to attach self to arg since it's dynamically linked + function_response = callable_func(**function_args) + self.ensure_read_only_block_not_modified( + new_memory=agent_state_copy.memory + ) # memory editing tools cannot edit read-only blocks + self.update_memory_if_changed(agent_state_copy.memory) + elif target_letta_tool.tool_type == ToolType.EXTERNAL_COMPOSIO: + action_name = generate_composio_action_from_func_name(target_letta_tool.name) + # Get entity ID from the agent_state + entity_id = None + for env_var in self.agent_state.secrets: + if env_var.key == COMPOSIO_ENTITY_ENV_VAR_KEY: + entity_id = env_var.value + # Get composio_api_key + composio_api_key = get_composio_api_key(actor=self.user, logger=self.logger) + function_response = execute_composio_action( + action_name=action_name, args=function_args, api_key=composio_api_key, entity_id=entity_id + ) + elif target_letta_tool.tool_type == ToolType.EXTERNAL_MCP: + # Get the server name from the tool tag + # TODO make a property instead? + server_name = target_letta_tool.tags[0].split(":")[1] + + # Get the MCPClient from the server's handle + # TODO these don't get raised properly + if not self.mcp_clients: + raise ValueError("No MCP client available to use") + if server_name not in self.mcp_clients: + raise ValueError(f"Unknown MCP server name: {server_name}") + mcp_client = self.mcp_clients[server_name] + + # Check that tool exists + available_tools = mcp_client.list_tools() + available_tool_names = [t.name for t in available_tools] + if function_name not in available_tool_names: + raise ValueError( + f"{function_name} is not available in MCP server {server_name}. Please check your `~/.letta/mcp_config.json` file." + ) + + function_response, is_error = mcp_client.execute_tool(tool_name=function_name, tool_args=function_args) + return ToolExecutionResult( + status="error" if is_error else "success", + func_return=function_response, + ) + else: + try: + # Parse the source code to extract function annotations + annotations = get_function_annotations_from_source(target_letta_tool.source_code, function_name) + # Coerce the function arguments to the correct types based on the annotations + function_args = coerce_dict_args_by_annotations(function_args, annotations) + except ValueError as e: + self.logger.debug(f"Error coercing function arguments: {e}") + + # execute tool in a sandbox + # TODO: allow agent_state to specify which sandbox to execute tools in + # TODO: This is only temporary, can remove after we publish a pip package with this object + agent_state_copy = self.agent_state.__deepcopy__() + agent_state_copy.tools = [] + agent_state_copy.tool_rules = [] + + tool_execution_result = ToolExecutionSandbox(function_name, function_args, self.user, tool_object=target_letta_tool).run( + agent_state=agent_state_copy + ) + assert orig_memory_str == self.agent_state.memory.compile(), "Memory should not be modified in a sandbox tool" + if tool_execution_result.agent_state is not None: + self.update_memory_if_changed(tool_execution_result.agent_state.memory) + return tool_execution_result + except Exception as e: + # Need to catch error here, or else trunction wont happen + # TODO: modify to function execution error + function_response = get_friendly_error_msg( + function_name=function_name, exception_name=type(e).__name__, exception_message=str(e) + ) + return ToolExecutionResult( + status="error", + func_return=function_response, + stderr=[traceback.format_exc()], + ) + + return ToolExecutionResult( + status="success", + func_return=function_response, + ) + + +def save_agent(agent: Agent): + """Save agent to metadata store""" + agent_state = agent.agent_state + assert isinstance(agent_state.memory, Memory), f"Memory is not a Memory object: {type(agent_state.memory)}" + + # TODO: move this to agent manager + # TODO: Completely strip out metadata + # convert to persisted model + agent_manager = AgentManager() + update_agent = UpdateAgent( + name=agent_state.name, + tool_ids=[t.id for t in agent_state.tools], + source_ids=[s.id for s in agent_state.sources], + block_ids=[b.id for b in agent_state.memory.blocks], + tags=agent_state.tags, + system=agent_state.system, + tool_rules=agent_state.tool_rules, + llm_config=agent_state.llm_config, + embedding_config=agent_state.embedding_config, + message_ids=agent_state.message_ids, + description=agent_state.description, + metadata=agent_state.metadata, + # TODO: Add this back in later + # tool_exec_environment_variables=agent_state.get_agent_env_vars_as_dict(), + ) + agent_manager.update_agent(agent_id=agent_state.id, agent_update=update_agent, actor=agent.user) + + +def strip_name_field_from_user_message(user_message_text: str) -> Tuple[str, Optional[str]]: + """If 'name' exists in the JSON string, remove it and return the cleaned text + name value""" + try: + user_message_json = dict(json_loads(user_message_text)) + # Special handling for AutoGen messages with 'name' field + # Treat 'name' as a special field + # If it exists in the input message, elevate it to the 'message' level + name = user_message_json.pop("name", None) + clean_message = json_dumps(user_message_json) + return clean_message, name + + except Exception as e: + print(f"{CLI_WARNING_PREFIX}handling of 'name' field failed with: {e}") + raise e + + +def validate_json(user_message_text: str) -> str: + """Make sure that the user input message is valid JSON""" + try: + user_message_json = dict(json_loads(user_message_text)) + user_message_json_val = json_dumps(user_message_json) + return user_message_json_val + except Exception as e: + print(f"{CLI_WARNING_PREFIX}couldn't parse user input message as JSON: {e}") + raise e diff --git a/letta/agents/__init__.py b/letta/agents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/letta/agents/agent_loop.py b/letta/agents/agent_loop.py new file mode 100644 index 0000000..8414a8b --- /dev/null +++ b/letta/agents/agent_loop.py @@ -0,0 +1,63 @@ +from typing import TYPE_CHECKING + +from letta.agents.base_agent_v2 import BaseAgentV2 +from letta.agents.letta_agent_v2 import LettaAgentV2 +from letta.agents.letta_agent_v3 import LettaAgentV3 +from letta.groups.sleeptime_multi_agent_v3 import SleeptimeMultiAgentV3 +from letta.groups.sleeptime_multi_agent_v4 import SleeptimeMultiAgentV4 +from letta.schemas.agent import AgentState +from letta.schemas.enums import AgentType + +if TYPE_CHECKING: + from letta.orm import User + + +class AgentLoop: + """Factory class for instantiating the agent execution loop based on agent type""" + + @staticmethod + def load(agent_state: AgentState, actor: "User") -> BaseAgentV2: + if agent_state.agent_type in [AgentType.letta_v1_agent, AgentType.sleeptime_agent]: + if agent_state.enable_sleeptime: + if agent_state.multi_agent_group is None: + # Agent has sleeptime enabled but no group - fall back to non-sleeptime agent + from letta.log import get_logger + + logger = get_logger(__name__) + logger.warning( + f"Agent {agent_state.id} has enable_sleeptime=True but multi_agent_group is None. " + f"Falling back to standard LettaAgentV3." + ) + return LettaAgentV3( + agent_state=agent_state, + actor=actor, + ) + return SleeptimeMultiAgentV4( + agent_state=agent_state, + actor=actor, + group=agent_state.multi_agent_group, + ) + return LettaAgentV3( + agent_state=agent_state, + actor=actor, + ) + elif agent_state.enable_sleeptime and agent_state.agent_type != AgentType.voice_convo_agent: + if agent_state.multi_agent_group is None: + # Agent has sleeptime enabled but no group - fall back to non-sleeptime agent + from letta.log import get_logger + + logger = get_logger(__name__) + logger.warning( + f"Agent {agent_state.id} has enable_sleeptime=True but multi_agent_group is None. " + f"Falling back to standard LettaAgentV2." + ) + return LettaAgentV2( + agent_state=agent_state, + actor=actor, + ) + return SleeptimeMultiAgentV3(agent_state=agent_state, actor=actor, group=agent_state.multi_agent_group) + else: + return LettaAgentV2( + agent_state=agent_state, + actor=actor, + ) diff --git a/letta/agents/base_agent.py b/letta/agents/base_agent.py new file mode 100644 index 0000000..e9373b3 --- /dev/null +++ b/letta/agents/base_agent.py @@ -0,0 +1,195 @@ +from abc import ABC, abstractmethod +from typing import Any, AsyncGenerator, List, Optional, Union + +import openai + +from letta.constants import DEFAULT_MAX_STEPS +from letta.helpers import ToolRulesSolver +from letta.helpers.datetime_helpers import get_utc_time +from letta.log import get_logger +from letta.prompts.prompt_generator import PromptGenerator +from letta.schemas.agent import AgentState +from letta.schemas.enums import MessageStreamStatus +from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage +from letta.schemas.letta_message_content import TextContent +from letta.schemas.letta_response import LettaResponse +from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType +from letta.schemas.message import Message, MessageCreate, MessageUpdate +from letta.schemas.provider_trace import BillingContext +from letta.schemas.usage import LettaUsageStatistics +from letta.schemas.user import User +from letta.services.agent_manager import AgentManager +from letta.services.message_manager import MessageManager +from letta.services.passage_manager import PassageManager +from letta.utils import united_diff + +logger = get_logger(__name__) + + +class BaseAgent(ABC): + """ + Abstract base class for AI agents, handling message management, tool execution, + and context tracking. + """ + + def __init__( + self, + agent_id: str, + # TODO: Make required once client refactor hits + openai_client: Optional[openai.AsyncClient], + message_manager: MessageManager, + agent_manager: AgentManager, + actor: User, + ): + self.agent_id = agent_id + self.openai_client = openai_client + self.message_manager = message_manager + self.agent_manager = agent_manager + # TODO: Pass this in + self.passage_manager = PassageManager() + self.actor = actor + self.logger = get_logger(agent_id) + self.client_skills: list = [] + self.conversation_id: str | None = None + + @abstractmethod + async def step( + self, + input_messages: List[MessageCreate], + max_steps: int = DEFAULT_MAX_STEPS, + run_id: Optional[str] = None, + billing_context: "BillingContext | None" = None, + ) -> LettaResponse: + """ + Main execution loop for the agent. + """ + raise NotImplementedError + + @abstractmethod + async def step_stream( + self, input_messages: List[MessageCreate], max_steps: int = DEFAULT_MAX_STEPS + ) -> AsyncGenerator[Union[LettaMessage, LegacyLettaMessage, MessageStreamStatus], None]: + """ + Main streaming execution loop for the agent. + """ + raise NotImplementedError + + @staticmethod + def pre_process_input_message(input_messages: List[MessageCreate]) -> Any: + """ + Pre-process function to run on the input_message. + """ + + def get_content(message: MessageCreate) -> str: + if isinstance(message.content, str): + return message.content + elif message.content and len(message.content) == 1 and isinstance(message.content[0], TextContent): + return message.content[0].text + else: + return "" + + return [{"role": input_message.role.value, "content": get_content(input_message)} for input_message in input_messages] + + async def _rebuild_memory_async( + self, + in_context_messages: List[Message], + agent_state: AgentState, + tool_rules_solver: Optional[ToolRulesSolver] = None, + num_messages: Optional[int] = None, # storing these calculations is specific to the voice agent + num_archival_memories: Optional[int] = None, + ) -> List[Message]: + """ + Async version of function above. For now before breaking up components, changes should be made in both places. + """ + try: + # [DB Call] loading blocks (modifies: agent_state.memory.blocks) + agent_state = await self.agent_manager.refresh_memory_async(agent_state=agent_state, actor=self.actor) + + tool_constraint_block = None + if tool_rules_solver is not None: + tool_constraint_block = tool_rules_solver.compile_tool_rule_prompts() + + # compile archive tags if there's an attached archive + from letta.services.archive_manager import ArchiveManager + + archive_manager = ArchiveManager() + archive = await archive_manager.get_default_archive_for_agent_async( + agent_id=agent_state.id, + actor=self.actor, + ) + + if archive: + archive_tags = await self.passage_manager.get_unique_tags_for_archive_async( + archive_id=archive.id, + actor=self.actor, + ) + else: + archive_tags = None + + # TODO: This is a pretty brittle pattern established all over our code, need to get rid of this + curr_system_message = in_context_messages[0] + curr_system_message_text = curr_system_message.content[0].text + + # generate memory string with current state for comparison + curr_memory_str = agent_state.memory.compile( + tool_usage_rules=tool_constraint_block, + sources=agent_state.sources, + max_files_open=agent_state.max_files_open, + llm_config=agent_state.llm_config, + ) + + system_prompt_changed = agent_state.system not in curr_system_message_text + memory_changed = curr_memory_str not in curr_system_message_text + if (not system_prompt_changed) and (not memory_changed): + logger.debug( + f"Memory and sources haven't changed for agent id={agent_state.id} and actor=({self.actor.id}, {self.actor.name}), skipping system prompt rebuild" + ) + return in_context_messages + + memory_edit_timestamp = get_utc_time() + + # size of messages and archival memories + if num_messages is None: + num_messages = await self.message_manager.size_async(actor=self.actor, agent_id=agent_state.id) + if num_archival_memories is None: + num_archival_memories = await self.passage_manager.agent_passage_size_async(actor=self.actor, agent_id=agent_state.id) + + new_system_message_str = PromptGenerator.get_system_message_from_compiled_memory( + system_prompt=agent_state.system, + memory_with_sources=curr_memory_str, + agent_id=agent_state.id, + conversation_id=self.conversation_id or "default", + in_context_memory_last_edit=memory_edit_timestamp, + timezone=agent_state.timezone, + previous_message_count=num_messages - len(in_context_messages), + archival_memory_size=num_archival_memories, + archive_tags=archive_tags, + ) + + diff = united_diff(curr_system_message_text, new_system_message_str) + if len(diff) > 0: + logger.debug(f"Rebuilding system with new memory...\nDiff:\n{diff}") + + # [DB Call] Update Messages + new_system_message = await self.message_manager.update_message_by_id_async( + curr_system_message.id, + message_update=MessageUpdate(content=new_system_message_str), + actor=self.actor, + project_id=agent_state.project_id, + ) + return [new_system_message, *in_context_messages[1:]] + + else: + return in_context_messages + except: + logger.exception(f"Failed to rebuild memory for agent id={agent_state.id} and actor=({self.actor.id}, {self.actor.name})") + raise + + def get_finish_chunks_for_stream(self, usage: LettaUsageStatistics, stop_reason: Optional[LettaStopReason] = None): + if stop_reason is None: + stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + return [ + stop_reason.model_dump_json(), + usage.model_dump_json(), + MessageStreamStatus.done.value, + ] diff --git a/letta/agents/base_agent_v2.py b/letta/agents/base_agent_v2.py new file mode 100644 index 0000000..3d7fac2 --- /dev/null +++ b/letta/agents/base_agent_v2.py @@ -0,0 +1,105 @@ +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, AsyncGenerator + +from letta.constants import DEFAULT_MAX_STEPS +from letta.log import get_logger +from letta.schemas.agent import AgentState +from letta.schemas.enums import MessageStreamStatus +from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, MessageType +from letta.schemas.letta_response import LettaResponse +from letta.schemas.message import MessageCreate +from letta.schemas.user import User + +if TYPE_CHECKING: + from letta.schemas.letta_request import ClientSkillSchema, ClientToolSchema + from letta.schemas.provider_trace import BillingContext + + +class BaseAgentV2(ABC): + """ + Abstract base class for the main agent execution loop for letta agents, handling + message management, llm api request, tool execution, and context tracking. + """ + + def __init__(self, agent_state: AgentState, actor: User): + self.agent_state = agent_state + self.actor = actor + self.logger = get_logger(agent_state.id) + self.conversation_id: str | None = None + + @property + def agent_id(self) -> str: + """Return the agent ID for backward compatibility with code expecting self.agent_id.""" + return self.agent_state.id + + @abstractmethod + async def build_request( + self, + input_messages: list[MessageCreate], + client_skills: list["ClientSkillSchema"] | None = None, + client_tools: list["ClientToolSchema"] | None = None, + conversation_id: str | None = None, + override_system: str | None = None, + ) -> dict: + """ + Execute the agent loop in dry_run mode, returning just the generated request + payload sent to the underlying llm provider. + """ + raise NotImplementedError + + @abstractmethod + async def step( + self, + input_messages: list[MessageCreate], + max_steps: int = DEFAULT_MAX_STEPS, + run_id: str | None = None, + use_assistant_message: bool = True, + include_return_message_types: list[MessageType] | None = None, + request_start_timestamp_ns: int | None = None, + client_tools: list["ClientToolSchema"] | None = None, + client_skills: list["ClientSkillSchema"] | None = None, + override_system: str | None = None, + include_compaction_messages: bool = False, # Not used in V2, but accepted for API compatibility + billing_context: "BillingContext | None" = None, + ) -> LettaResponse: + """ + Execute the agent loop in blocking mode, returning all messages at once. + + Args: + client_tools: Optional list of client-side tools. When called, execution pauses + for client to provide tool returns. + include_compaction_messages: Not used in V2, but accepted for API compatibility. + """ + raise NotImplementedError + + @abstractmethod + async def stream( + self, + input_messages: list[MessageCreate], + max_steps: int = DEFAULT_MAX_STEPS, + stream_tokens: bool = False, + run_id: str | None = None, + use_assistant_message: bool = True, + include_return_message_types: list[MessageType] | None = None, + request_start_timestamp_ns: int | None = None, + conversation_id: str | None = None, + client_tools: list["ClientToolSchema"] | None = None, + client_skills: list["ClientSkillSchema"] | None = None, + override_system: str | None = None, + include_compaction_messages: bool = False, # Not used in V2, but accepted for API compatibility + billing_context: "BillingContext | None" = None, + openai_responses_websocket: bool = False, + ) -> AsyncGenerator[LettaMessage | LegacyLettaMessage | MessageStreamStatus, None]: + """ + Execute the agent loop in streaming mode, yielding chunks as they become available. + If stream_tokens is True, individual tokens are streamed as they arrive from the LLM, + providing the lowest latency experience, otherwise each complete step (reasoning + + tool call + tool return) is yielded as it completes. + + Args: + client_tools: Optional list of client-side tools. When called, execution pauses + for client to provide tool returns. + include_compaction_messages: Not used in V2, but accepted for API compatibility. + openai_responses_websocket: If True, use WebSocket transport for OpenAI Responses API. + """ + raise NotImplementedError diff --git a/letta/agents/ephemeral_agent.py b/letta/agents/ephemeral_agent.py new file mode 100644 index 0000000..d951b43 --- /dev/null +++ b/letta/agents/ephemeral_agent.py @@ -0,0 +1,72 @@ +from typing import AsyncGenerator, Dict, List + +import openai + +from letta.agents.base_agent import BaseAgent +from letta.schemas.agent import AgentState +from letta.schemas.enums import MessageRole +from letta.schemas.letta_message_content import TextContent +from letta.schemas.message import Message, MessageCreate +from letta.schemas.openai.chat_completion_request import ChatCompletionRequest +from letta.schemas.user import User +from letta.services.agent_manager import AgentManager +from letta.services.message_manager import MessageManager + + +class EphemeralAgent(BaseAgent): + """ + A stateless agent (thin wrapper around OpenAI) + + # TODO: Extend to more clients + """ + + def __init__( + self, + agent_id: str, + openai_client: openai.AsyncClient, + message_manager: MessageManager, + agent_manager: AgentManager, + actor: User, + ): + super().__init__( + agent_id=agent_id, + openai_client=openai_client, + message_manager=message_manager, + agent_manager=agent_manager, + actor=actor, + ) + + async def step(self, input_messages: List[MessageCreate]) -> List[Message]: + """ + Synchronous method that takes a user's input text and returns a summary from OpenAI. + Returns a list of ephemeral Message objects containing both the user text and the assistant summary. + """ + agent_state = self.agent_manager.get_agent_by_id(agent_id=self.agent_id, actor=self.actor) + + openai_messages = self.pre_process_input_message(input_messages=input_messages) + request = self._build_openai_request(openai_messages, agent_state) + + chat_completion = await self.openai_client.chat.completions.create(**request.model_dump(exclude_unset=True)) + + return [ + Message( + role=MessageRole.assistant, + content=[TextContent(text=chat_completion.choices[0].message.content.strip())], + ) + ] + + def _build_openai_request(self, openai_messages: List[Dict], agent_state: AgentState) -> ChatCompletionRequest: + openai_request = ChatCompletionRequest( + model=agent_state.llm_config.model, + messages=openai_messages, + user=self.actor.id, + max_completion_tokens=agent_state.llm_config.max_tokens, + temperature=agent_state.llm_config.temperature, + ) + return openai_request + + async def step_stream(self, input_messages: List[MessageCreate]) -> AsyncGenerator[str, None]: + """ + This agent is synchronous-only. If called in an async context, raise an error. + """ + raise NotImplementedError("EphemeralAgent does not support async step.") diff --git a/letta/agents/ephemeral_summary_agent.py b/letta/agents/ephemeral_summary_agent.py new file mode 100644 index 0000000..86b2b90 --- /dev/null +++ b/letta/agents/ephemeral_summary_agent.py @@ -0,0 +1,114 @@ +from typing import AsyncGenerator, List + +from letta.agents.base_agent import BaseAgent +from letta.constants import DEFAULT_MAX_STEPS +from letta.helpers.message_helper import convert_message_creates_to_messages +from letta.llm_api.llm_client import LLMClient +from letta.log import get_logger +from letta.orm.errors import NoResultFound +from letta.prompts.gpt_system import get_system_text +from letta.schemas.block import Block, BlockUpdate +from letta.schemas.enums import LLMCallType, MessageRole +from letta.schemas.letta_message_content import TextContent +from letta.schemas.message import Message, MessageCreate +from letta.schemas.user import User +from letta.services.agent_manager import AgentManager +from letta.services.block_manager import BlockManager +from letta.services.message_manager import MessageManager + +logger = get_logger(__name__) + + +class EphemeralSummaryAgent(BaseAgent): + """ + A stateless summarization agent that utilizes the caller's LLM client to summarize the conversation. + TODO (cliandy): allow the summarizer to use another llm_config from the main agent maybe? + """ + + def __init__( + self, + target_block_label: str, + agent_id: str, + message_manager: MessageManager, + agent_manager: AgentManager, + block_manager: BlockManager, + actor: User, + ): + super().__init__( + agent_id=agent_id, + openai_client=None, + message_manager=message_manager, + agent_manager=agent_manager, + actor=actor, + ) + self.target_block_label = target_block_label + self.block_manager = block_manager + + async def step(self, input_messages: List[MessageCreate], max_steps: int = DEFAULT_MAX_STEPS) -> List[Message]: + if len(input_messages) > 1: + raise ValueError("Can only invoke EphemeralSummaryAgent with a single summarization message.") + + # Check block existence + try: + block = await self.agent_manager.get_block_with_label_async( + agent_id=self.agent_id, block_label=self.target_block_label, actor=self.actor + ) + except NoResultFound: + block = await self.block_manager.create_or_update_block_async( + block=Block( + value="", label=self.target_block_label, description="Contains recursive summarizations of the conversation so far" + ), + actor=self.actor, + ) + await self.agent_manager.attach_block_async(agent_id=self.agent_id, block_id=block.id, actor=self.actor) + + if block.value: + input_message = input_messages[0] + input_message.content[0].text += f"\n\n--- Previous Summary ---\n{block.value}\n" + + # Gets the LLMCLient based on the calling agent's LLM Config + agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=self.agent_id, actor=self.actor) + llm_client = LLMClient.create( + provider_type=agent_state.llm_config.model_endpoint_type, + put_inner_thoughts_first=True, + actor=self.actor, + ) + + system_message_create = MessageCreate( + role=MessageRole.system, + content=[TextContent(text=get_system_text("summary_system_prompt"))], + ) + messages = await convert_message_creates_to_messages( + message_creates=[system_message_create, *input_messages], + agent_id=self.agent_id, + timezone=agent_state.timezone, + run_id=None, # TODO: add this + ) + + request_data = llm_client.build_request_data(agent_state.agent_type, messages, agent_state.llm_config, tools=[]) + from letta.services.telemetry_manager import TelemetryManager + + llm_client.set_telemetry_context( + telemetry_manager=TelemetryManager(), + agent_id=self.agent_id, + agent_tags=agent_state.tags, + call_type=LLMCallType.summarization, + ) + response_data = await llm_client.request_async_with_telemetry(request_data, agent_state.llm_config) + response = await llm_client.convert_response_to_chat_completion(response_data, messages, agent_state.llm_config) + summary = response.choices[0].message.content.strip() + + await self.block_manager.update_block_async(block_id=block.id, block_update=BlockUpdate(value=summary), actor=self.actor) + + logger.debug("block:", block) + logger.debug("summary:", summary) + + return [ + Message( + role=MessageRole.assistant, + content=[TextContent(text=summary)], + ) + ] + + async def step_stream(self, input_messages: List[MessageCreate], max_steps: int = DEFAULT_MAX_STEPS) -> AsyncGenerator[str, None]: + raise NotImplementedError("EphemeralAgent does not support async step.") diff --git a/letta/agents/exceptions.py b/letta/agents/exceptions.py new file mode 100644 index 0000000..270cfc3 --- /dev/null +++ b/letta/agents/exceptions.py @@ -0,0 +1,6 @@ +class IncompatibleAgentType(ValueError): + def __init__(self, expected_type: str, actual_type: str): + message = f"Incompatible agent type: expected '{expected_type}', but got '{actual_type}'." + super().__init__(message) + self.expected_type = expected_type + self.actual_type = actual_type diff --git a/letta/agents/helpers.py b/letta/agents/helpers.py new file mode 100644 index 0000000..fae2769 --- /dev/null +++ b/letta/agents/helpers.py @@ -0,0 +1,543 @@ +import json +import xml.etree.ElementTree as ET +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple +from uuid import UUID, uuid4 + +if TYPE_CHECKING: + from letta.schemas.tool import Tool + +from letta.errors import LettaError, PendingApprovalError +from letta.helpers import ToolRulesSolver +from letta.log import get_logger +from letta.otel.tracing import trace_method +from letta.schemas.agent import AgentState +from letta.schemas.enums import MessageRole +from letta.schemas.letta_message import MessageType +from letta.schemas.letta_message_content import TextContent +from letta.schemas.letta_response import LettaResponse +from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType +from letta.schemas.message import ApprovalCreate, Message, MessageCreate, MessageCreateBase +from letta.schemas.tool_execution_result import ToolExecutionResult +from letta.schemas.usage import LettaUsageStatistics +from letta.schemas.user import User +from letta.server.rest_api.utils import create_approval_response_message_from_input, create_input_messages +from letta.services.message_manager import MessageManager + +logger = get_logger(__name__) + + +def _create_letta_response( + new_in_context_messages: list[Message], + use_assistant_message: bool, + usage: LettaUsageStatistics, + stop_reason: Optional[LettaStopReason] = None, + include_return_message_types: Optional[List[MessageType]] = None, +) -> LettaResponse: + """ + Converts the newly created/persisted messages into a LettaResponse. + """ + # NOTE: hacky solution to avoid returning heartbeat messages and the original user message + filter_user_messages = [m for m in new_in_context_messages if m.role != "user"] + + # Convert to Letta messages first + response_messages = Message.to_letta_messages_from_list( + messages=filter_user_messages, use_assistant_message=use_assistant_message, reverse=False + ) + # Filter approval response messages + response_messages = [m for m in response_messages if m.message_type != "approval_response_message"] + + # Apply message type filtering if specified + if include_return_message_types is not None: + response_messages = [msg for msg in response_messages if msg.message_type in include_return_message_types] + if stop_reason is None: + stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + return LettaResponse(messages=response_messages, stop_reason=stop_reason, usage=usage) + + +async def _prepare_in_context_messages_async( + input_messages: List[MessageCreate], + agent_state: AgentState, + message_manager: MessageManager, + actor: User, + run_id: str, +) -> Tuple[List[Message], List[Message]]: + """ + Prepares in-context messages for an agent, based on the current state and a new user input. + Async version of _prepare_in_context_messages. + + Args: + input_messages (List[MessageCreate]): The new user input messages to process. + agent_state (AgentState): The current state of the agent, including message buffer config. + message_manager (MessageManager): The manager used to retrieve and create messages. + actor (User): The user performing the action, used for access control and attribution. + run_id (str): The run ID associated with this message processing. + + Returns: + Tuple[List[Message], List[Message]]: A tuple containing: + - The current in-context messages (existing context for the agent). + - The new in-context messages (messages created from the new input). + """ + + if agent_state.message_buffer_autoclear: + # If autoclear is enabled, only include the most recent system message (usually at index 0) + current_in_context_messages = [await message_manager.get_message_by_id_async(message_id=agent_state.message_ids[0], actor=actor)] + else: + # Otherwise, include the full list of messages by ID for context + current_in_context_messages = await message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor) + + # Create a new user message from the input and store it + input_msgs = await create_input_messages( + input_messages=input_messages, agent_id=agent_state.id, timezone=agent_state.timezone, run_id=run_id, actor=actor + ) + new_in_context_messages = await message_manager.create_many_messages_async( + input_msgs, + actor=actor, + project_id=agent_state.project_id, + ) + + return current_in_context_messages, new_in_context_messages + + +@trace_method +def validate_persisted_tool_call_ids(tool_return_message: Message, approval_response_message: ApprovalCreate) -> bool: + persisted_tool_returns = tool_return_message.tool_returns + if not persisted_tool_returns: + return False + persisted_tool_call_ids = [tool_return.tool_call_id for tool_return in persisted_tool_returns] + + approval_responses = approval_response_message.approvals + if not approval_responses: + return False + approval_response_tool_call_ids = [approval_response.tool_call_id for approval_response in approval_responses] + + request_response_diff = set(persisted_tool_call_ids).symmetric_difference(set(approval_response_tool_call_ids)) + if request_response_diff: + return False + + return True + + +@trace_method +def validate_approval_tool_call_ids(approval_request_message: Message, approval_response_message: ApprovalCreate): + approval_requests = approval_request_message.tool_calls + if approval_requests: + approval_request_tool_call_ids = [approval_request.id for approval_request in approval_requests] + elif approval_request_message.tool_call_id: + approval_request_tool_call_ids = [approval_request_message.tool_call_id] + else: + raise ValueError( + f"Invalid tool call IDs. Approval request message '{approval_request_message.id}' does not contain any tool calls." + ) + + approval_responses = approval_response_message.approvals + if not approval_responses: + raise ValueError("Invalid approval response. Approval response message does not contain any approvals.") + approval_response_tool_call_ids = [approval_response.tool_call_id for approval_response in approval_responses] + + request_response_diff = set(approval_request_tool_call_ids).symmetric_difference(set(approval_response_tool_call_ids)) + if request_response_diff: + if len(approval_request_tool_call_ids) == 1 and approval_response_tool_call_ids[0] == approval_request_message.id: + # legacy case where we used to use message id instead of tool call id + return + + raise ValueError( + f"Invalid tool call IDs. Expected '{approval_request_tool_call_ids}', but received '{approval_response_tool_call_ids}'." + ) + + +@trace_method +async def _prepare_in_context_messages_no_persist_async( + input_messages: List[MessageCreateBase], + agent_state: AgentState, + message_manager: MessageManager, + actor: User, + run_id: Optional[str] = None, + conversation_id: Optional[str] = None, +) -> Tuple[List[Message], List[Message]]: + """ + Prepares in-context messages for an agent, based on the current state and a new user input. + + When conversation_id is provided, messages are loaded from the conversation_messages + table instead of agent_state.message_ids. + + Args: + input_messages (List[MessageCreate]): The new user input messages to process. + agent_state (AgentState): The current state of the agent, including message buffer config. + message_manager (MessageManager): The manager used to retrieve and create messages. + actor (User): The user performing the action, used for access control and attribution. + run_id (str): The run ID associated with this message processing. + conversation_id (str): Optional conversation ID to load messages from. + + Returns: + Tuple[List[Message], List[Message]]: A tuple containing: + - The current in-context messages (existing context for the agent). + - The new in-context messages (messages created from the new input). + """ + + if conversation_id: + # Conversation mode: load messages from conversation_messages table + from letta.services.conversation_manager import ConversationManager + + conversation_manager = ConversationManager() + message_ids = await conversation_manager.get_message_ids_for_conversation( + conversation_id=conversation_id, + actor=actor, + ) + + if agent_state.message_buffer_autoclear and message_ids: + # If autoclear is enabled, only include the system message + current_in_context_messages = [await message_manager.get_message_by_id_async(message_id=message_ids[0], actor=actor)] + elif message_ids: + # Otherwise, include the full list of messages from the conversation + current_in_context_messages = await message_manager.get_messages_by_ids_async(message_ids=message_ids, actor=actor) + else: + # No messages in conversation yet (fallback) - compile a new system message + # Normally this is handled at conversation creation time, but this covers + # edge cases where a conversation exists without a system message. + system_message = await conversation_manager.compile_and_save_system_message_for_conversation( + conversation_id=conversation_id, + agent_id=agent_state.id, + actor=actor, + agent_state=agent_state, + message_manager=message_manager, + ) + + current_in_context_messages = [system_message] + else: + # Default mode: load messages from agent_state.message_ids + if not agent_state.message_ids: + raise LettaError( + message=f"Agent {agent_state.id} has no in-context messages. " + "This typically means the agent's system message was not initialized correctly.", + ) + if agent_state.message_buffer_autoclear: + # If autoclear is enabled, only include the most recent system message (usually at index 0) + current_in_context_messages = [ + await message_manager.get_message_by_id_async(message_id=agent_state.message_ids[0], actor=actor) + ] + else: + # Otherwise, include the full list of messages by ID for context + current_in_context_messages = await message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor) + + # Convert ToolReturnCreate to ApprovalCreate for unified processing + if input_messages[0].type == "tool_return": + tool_return_msg = input_messages[0] + input_messages = [ + ApprovalCreate(approvals=tool_return_msg.tool_returns), + *input_messages[1:], + ] + + # Check for approval-related message validation + if input_messages[0].type == "approval": + # User is trying to send an approval response + if current_in_context_messages and current_in_context_messages[-1].role != "approval": + # No pending approval request - check if this is an idempotent retry + # Check last few messages for a tool return matching the approval's tool_call_ids + # (approved tool return should be recent, but server-side tool calls may come after it) + approval_already_processed = False + recent_messages = current_in_context_messages[-10:] # Only check last 10 messages + for msg in reversed(recent_messages): + if msg.role == "tool" and validate_persisted_tool_call_ids(msg, input_messages[0]): + logger.info( + f"Idempotency check: Found matching tool return in recent in-context history. " + f"tool_returns={msg.tool_returns}, approval_response.approvals={input_messages[0].approvals}" + ) + approval_already_processed = True + break + + # If not found in context and summarization just happened, check full history + non_system_summary_messages = [ + m for m in current_in_context_messages if m.role not in (MessageRole.system, MessageRole.summary) + ] + if not approval_already_processed and len(non_system_summary_messages) == 0: + last_tool_messages = await message_manager.list_messages( + actor=actor, + agent_id=agent_state.id, + roles=[MessageRole.tool], + limit=1, + ascending=False, # Most recent first + ) + if len(last_tool_messages) == 1 and validate_persisted_tool_call_ids(last_tool_messages[0], input_messages[0]): + logger.info( + f"Idempotency check: Found matching tool return in full history (post-compaction). " + f"tool_returns={last_tool_messages[0].tool_returns}, approval_response.approvals={input_messages[0].approvals}" + ) + approval_already_processed = True + + if approval_already_processed: + # Approval already handled, just process follow-up messages if any or manually inject keep-alive message + follow_up = [m for m in input_messages[1:] if isinstance(m, MessageCreate)] + skipped = [m for m in input_messages[1:] if not isinstance(m, MessageCreate)] + if skipped: + logger.warning(f"Filtered {len(skipped)} non-MessageCreate follow-up messages: {[type(m).__name__ for m in skipped]}") + keep_alive_messages = follow_up or [ + MessageCreate( + role="user", + content=[ + TextContent( + text="Automated keep-alive ping. Ignore this message and continue from where you stopped." + ) + ], + ) + ] + new_in_context_messages = await create_input_messages( + input_messages=keep_alive_messages, agent_id=agent_state.id, timezone=agent_state.timezone, run_id=run_id, actor=actor + ) + return current_in_context_messages, new_in_context_messages + logger.warn( + f"Cannot process approval response: No tool call is currently awaiting approval. Last message: {current_in_context_messages[-1]}" + ) + raise ValueError( + "Cannot process approval response: No tool call is currently awaiting approval. " + "Please send a regular message to interact with the agent." + ) + validate_approval_tool_call_ids(current_in_context_messages[-1], input_messages[0]) + new_in_context_messages = await create_approval_response_message_from_input( + agent_state=agent_state, input_message=input_messages[0], run_id=run_id + ) + follow_up = [m for m in input_messages[1:] if isinstance(m, MessageCreate)] + skipped = [m for m in input_messages[1:] if not isinstance(m, MessageCreate)] + if skipped: + logger.warning(f"Filtered {len(skipped)} non-MessageCreate follow-up messages: {[type(m).__name__ for m in skipped]}") + if follow_up: + follow_up_messages = await create_input_messages( + input_messages=follow_up, agent_id=agent_state.id, timezone=agent_state.timezone, run_id=run_id, actor=actor + ) + new_in_context_messages.extend(follow_up_messages) + else: + # User is trying to send a regular message + if current_in_context_messages and current_in_context_messages[-1].is_approval_request(): + raise PendingApprovalError(pending_request_id=current_in_context_messages[-1].id) + + # Create a new user message from the input but dont store it yet + new_in_context_messages = await create_input_messages( + input_messages=input_messages, agent_id=agent_state.id, timezone=agent_state.timezone, run_id=run_id, actor=actor + ) + + return current_in_context_messages, new_in_context_messages + + +def serialize_message_history(messages: List[str], context: str) -> str: + """ + Produce an XML document like: + + + + + + … + + + + """ + root = ET.Element("memory") + + msgs_el = ET.SubElement(root, "messages") + for msg in messages: + m = ET.SubElement(msgs_el, "message") + m.text = msg + + sum_el = ET.SubElement(root, "context") + sum_el.text = context + + # ET.tostring will escape reserved chars for you + return ET.tostring(root, encoding="unicode") + + +def deserialize_message_history(xml_str: str) -> Tuple[List[str], str]: + """ + Parse the XML back into (messages, context). Raises ValueError if tags are missing. + """ + try: + root = ET.fromstring(xml_str) + except ET.ParseError as e: + raise ValueError(f"Invalid XML: {e}") + + msgs_el = root.find("messages") + if msgs_el is None: + raise ValueError("Missing section") + + messages = [] + for m in msgs_el.findall("message"): + # .text may be None if empty, so coerce to empty string + messages.append(m.text or "") + + sum_el = root.find("context") + if sum_el is None: + raise ValueError("Missing section") + context = sum_el.text or "" + + return messages, context + + +def generate_step_id(uid: Optional[UUID] = None) -> str: + uid = uid or uuid4() + return f"step-{uid}" + + +def _safe_load_tool_call_str(tool_call_args_str: str) -> dict: + """Lenient JSON → dict with fallback to eval on assertion failure.""" + # Temp hack to gracefully handle parallel tool calling attempt, only take first one + if "}{" in tool_call_args_str: + tool_call_args_str = tool_call_args_str.split("}{", 1)[0] + "}" + + try: + tool_args = json.loads(tool_call_args_str) + if not isinstance(tool_args, dict): + # Load it again - this is due to sometimes Anthropic returning weird json @caren + tool_args = json.loads(tool_args) + except json.JSONDecodeError: + logger.error("Failed to JSON decode tool call argument string: %s", tool_call_args_str) + tool_args = {} + + return tool_args + + +def _json_type_matches(value: Any, expected_type: Any) -> bool: + """Basic JSON Schema type checking for common types. + + expected_type can be a string (e.g., "string") or a list (union). + This is intentionally lightweight; deeper validation can be added as needed. + """ + + def match_one(v: Any, t: str) -> bool: + if t == "string": + return isinstance(v, str) + if t == "integer": + # bool is subclass of int in Python; exclude + return isinstance(v, int) and not isinstance(v, bool) + if t == "number": + return (isinstance(v, int) and not isinstance(v, bool)) or isinstance(v, float) + if t == "boolean": + return isinstance(v, bool) + if t == "object": + return isinstance(v, dict) + if t == "array": + return isinstance(v, list) + if t == "null": + return v is None + # Fallback: don't over-reject on unknown types + return True + + if isinstance(expected_type, list): + return any(match_one(value, t) for t in expected_type) + if isinstance(expected_type, str): + return match_one(value, expected_type) + return True + + +def _schema_accepts_value(prop_schema: Dict[str, Any], value: Any) -> bool: + """Check if a value is acceptable for a property schema. + + Handles: type, enum, const, anyOf, oneOf (by shallow traversal). + """ + if prop_schema is None: + return True + + # const has highest precedence + if "const" in prop_schema: + return value == prop_schema["const"] + + # enums + if "enum" in prop_schema: + try: + return value in prop_schema["enum"] + except Exception: + return False + + # unions + for union_key in ("anyOf", "oneOf"): + if union_key in prop_schema and isinstance(prop_schema[union_key], list): + for sub in prop_schema[union_key]: + if _schema_accepts_value(sub, value): + return True + return False + + # type-based + if "type" in prop_schema: + if not _json_type_matches(value, prop_schema["type"]): + return False + + # No strict constraints specified: accept + return True + + +def merge_and_validate_prefilled_args(tool: "Tool", llm_args: Dict[str, Any], prefilled_args: Dict[str, Any]) -> Dict[str, Any]: + """Merge LLM-provided args with prefilled args from tool rules. + + - Overlapping keys are replaced by prefilled values (prefilled wins). + - Validates that prefilled keys exist on the tool schema and that values satisfy + basic JSON Schema constraints (type/enum/const/anyOf/oneOf). + - Returns merged args, or raises ValueError on invalid prefilled inputs. + """ + from letta.schemas.tool import Tool # local import to avoid circulars in type hints + + assert isinstance(tool, Tool) + schema = (tool.json_schema or {}).get("parameters", {}) + props: Dict[str, Any] = schema.get("properties", {}) if isinstance(schema, dict) else {} + + errors: list[str] = [] + for k, v in prefilled_args.items(): + if k not in props: + errors.append(f"Unknown argument '{k}' for tool '{tool.name}'.") + continue + if not _schema_accepts_value(props.get(k), v): + expected = props.get(k, {}).get("type") + errors.append(f"Invalid value for '{k}': {v!r} does not match expected schema type {expected!r}.") + + if errors: + raise ValueError("; ".join(errors)) + + merged = dict(llm_args or {}) + merged.update(prefilled_args) + return merged + + +def _pop_heartbeat(tool_args: dict) -> bool: + hb = tool_args.pop("request_heartbeat", False) + return str(hb).lower() == "true" if isinstance(hb, str) else bool(hb) + + +def _build_rule_violation_result(tool_name: str, valid: list[str], solver: ToolRulesSolver) -> ToolExecutionResult: + hint_lines = solver.guess_rule_violation(tool_name) + hint_txt = ("\n** Hint: Possible rules that were violated:\n" + "\n".join(f"\t- {h}" for h in hint_lines)) if hint_lines else "" + msg = f"[ToolConstraintError] Cannot call {tool_name}, valid tools include: {valid}.{hint_txt}" + return ToolExecutionResult(status="error", func_return=msg) + + +def _load_last_function_response(in_context_messages: list[Message]): + """Load the last function response from message history""" + for msg in reversed(in_context_messages): + if msg.role == MessageRole.tool and msg.content and len(msg.content) == 1 and isinstance(msg.content[0], TextContent): + text_content = msg.content[0].text + try: + response_json = json.loads(text_content) + if response_json.get("message"): + return response_json["message"] + except (json.JSONDecodeError, KeyError): + raise ValueError(f"Invalid JSON format in message: {text_content}") + return None + + +def _maybe_get_approval_messages(messages: list[Message]) -> Tuple[Message | None, Message | None]: + if len(messages) >= 2: + maybe_approval_request, maybe_approval_response = messages[-2], messages[-1] + if maybe_approval_request.role == "approval" and maybe_approval_response.role == "approval": + return maybe_approval_request, maybe_approval_response + return None, None + + +def _maybe_get_pending_tool_call_message(messages: list[Message]) -> Message | None: + """ + Only used in the case where hitl is invoked with parallel tool calling, + where agent calls some tools that require approval, and others that don't. + """ + if len(messages) >= 3: + maybe_tool_call_message = messages[-3] + if ( + maybe_tool_call_message.role == "assistant" + and maybe_tool_call_message.tool_calls is not None + and len(maybe_tool_call_message.tool_calls) > 0 + ): + return maybe_tool_call_message + return None diff --git a/letta/agents/letta_agent.py b/letta/agents/letta_agent.py new file mode 100644 index 0000000..9475e1f --- /dev/null +++ b/letta/agents/letta_agent.py @@ -0,0 +1,1983 @@ +import json +import uuid +from collections.abc import AsyncGenerator +from datetime import datetime +from typing import Optional, Union + +from openai import AsyncStream +from openai.types.chat import ChatCompletionChunk +from opentelemetry.trace import Span + +from letta.agents.base_agent import BaseAgent +from letta.agents.ephemeral_summary_agent import EphemeralSummaryAgent +from letta.agents.helpers import ( + _build_rule_violation_result, + _create_letta_response, + _pop_heartbeat, + _prepare_in_context_messages_no_persist_async, + _safe_load_tool_call_str, + generate_step_id, +) +from letta.constants import DEFAULT_MAX_STEPS, NON_USER_MSG_PREFIX, REQUEST_HEARTBEAT_PARAM +from letta.errors import ContextWindowExceededError, LLMError +from letta.helpers import ToolRulesSolver +from letta.helpers.datetime_helpers import AsyncTimer, get_utc_time, get_utc_timestamp_ns, ns_to_ms +from letta.helpers.reasoning_helper import scrub_inner_thoughts_from_messages +from letta.helpers.tool_execution_helper import enable_strict_mode +from letta.interfaces.anthropic_streaming_interface import AnthropicStreamingInterface +from letta.interfaces.openai_streaming_interface import OpenAIStreamingInterface +from letta.llm_api.llm_client import LLMClient +from letta.llm_api.llm_client_base import LLMClientBase +from letta.local_llm.constants import INNER_THOUGHTS_KWARG +from letta.log import get_logger +from letta.otel.context import get_ctx_attributes +from letta.otel.metric_registry import MetricRegistry +from letta.otel.tracing import log_event, trace_method, tracer +from letta.schemas.agent import AgentState, UpdateAgent +from letta.schemas.enums import JobStatus, LLMCallType, ProviderType, StepStatus, ToolType +from letta.schemas.letta_message import MessageType +from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, RedactedReasoningContent, TextContent +from letta.schemas.letta_response import LettaResponse +from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType +from letta.schemas.llm_config import LLMConfig +from letta.schemas.message import Message, MessageCreateBase +from letta.schemas.openai.chat_completion_response import ( + FunctionCall, + ToolCall, + UsageStatistics, + UsageStatisticsCompletionTokenDetails, + UsageStatisticsPromptTokenDetails, +) +from letta.schemas.provider_trace import BillingContext +from letta.schemas.step import StepProgression +from letta.schemas.step_metrics import StepMetrics +from letta.schemas.tool_execution_result import ToolExecutionResult +from letta.schemas.usage import LettaUsageStatistics +from letta.schemas.user import User +from letta.server.rest_api.utils import ( + create_approval_request_message_from_llm_response, + create_letta_messages_from_llm_response, +) +from letta.services.agent_manager import AgentManager +from letta.services.block_manager import BlockManager +from letta.services.helpers.tool_parser_helper import runtime_override_tool_json_schema +from letta.services.job_manager import JobManager +from letta.services.message_manager import MessageManager +from letta.services.passage_manager import PassageManager +from letta.services.step_manager import NoopStepManager, StepManager +from letta.services.summarizer.enums import SummarizationMode +from letta.services.summarizer.summarizer import Summarizer +from letta.services.telemetry_manager import NoopTelemetryManager, TelemetryManager +from letta.services.tool_executor.tool_execution_manager import ToolExecutionManager +from letta.settings import model_settings, settings, summarizer_settings +from letta.system import package_function_response +from letta.types import JsonDict +from letta.utils import log_telemetry, validate_function_response + +logger = get_logger(__name__) + +DEFAULT_SUMMARY_BLOCK_LABEL = "conversation_summary" + + +class LettaAgent(BaseAgent): + def __init__( + self, + agent_id: str, + message_manager: MessageManager, + agent_manager: AgentManager, + block_manager: BlockManager, + job_manager: JobManager, + passage_manager: PassageManager, + actor: User, + step_manager: StepManager = NoopStepManager(), + telemetry_manager: TelemetryManager = NoopTelemetryManager(), + current_run_id: str | None = None, + ## summarizer settings + summarizer_mode: SummarizationMode = summarizer_settings.mode, + # for static_buffer mode + summary_block_label: str = DEFAULT_SUMMARY_BLOCK_LABEL, + message_buffer_limit: int = summarizer_settings.message_buffer_limit, + message_buffer_min: int = summarizer_settings.message_buffer_min, + enable_summarization: bool = summarizer_settings.enable_summarization, + max_summarization_retries: int = summarizer_settings.max_summarization_retries, + # for partial_evict mode + partial_evict_summarizer_percentage: float = summarizer_settings.partial_evict_summarizer_percentage, + ): + super().__init__(agent_id=agent_id, openai_client=None, message_manager=message_manager, agent_manager=agent_manager, actor=actor) + + # TODO: Make this more general, factorable + # Summarizer settings + self.block_manager = block_manager + self.job_manager = job_manager + self.passage_manager = passage_manager + self.step_manager = step_manager + self.telemetry_manager = telemetry_manager + self.job_manager = job_manager + self.current_run_id = current_run_id + self.response_messages: list[Message] = [] + + self.last_function_response = None + + # Cached archival memory/message size + self.num_messages = None + self.num_archival_memories = None + + self.summarization_agent = None + self.summary_block_label = summary_block_label + self.max_summarization_retries = max_summarization_retries + self.logger = get_logger(agent_id) + + # TODO: Expand to more + if enable_summarization and model_settings.openai_api_key: + self.summarization_agent = EphemeralSummaryAgent( + target_block_label=self.summary_block_label, + agent_id=agent_id, + block_manager=self.block_manager, + message_manager=self.message_manager, + agent_manager=self.agent_manager, + actor=self.actor, + ) + + self.summarizer = Summarizer( + mode=summarizer_mode, + # TODO consolidate to not use this, or push it into the Summarizer() class + summarizer_agent=self.summarization_agent, + # TODO: Make this configurable + message_buffer_limit=message_buffer_limit, + message_buffer_min=message_buffer_min, + partial_evict_summarizer_percentage=partial_evict_summarizer_percentage, + agent_manager=self.agent_manager, + message_manager=self.message_manager, + actor=self.actor, + agent_id=self.agent_id, + ) + + async def _check_run_cancellation(self) -> bool: + """ + Check if the current run associated with this agent execution has been cancelled. + + Returns: + True if the run is cancelled, False otherwise (or if no run is associated) + """ + if not self.job_manager or not self.current_run_id: + return False + + try: + job = await self.job_manager.get_job_by_id_async(job_id=self.current_run_id, actor=self.actor) + return job.status == JobStatus.cancelled + except Exception as e: + # Log the error but don't fail the execution + logger.warning(f"Failed to check job cancellation status for job {self.current_run_id}: {e}") + return False + + @trace_method + async def step( + self, + input_messages: list[MessageCreateBase], + max_steps: int = DEFAULT_MAX_STEPS, + run_id: str | None = None, + use_assistant_message: bool = True, + request_start_timestamp_ns: int | None = None, + include_return_message_types: list[MessageType] | None = None, + dry_run: bool = False, + billing_context: "BillingContext | None" = None, + ) -> Union[LettaResponse, dict]: + # TODO (cliandy): pass in run_id and use at send_message endpoints for all step functions + agent_state = await self.agent_manager.get_agent_by_id_async( + agent_id=self.agent_id, + include_relationships=["tools", "memory", "tool_exec_environment_variables", "sources"], + actor=self.actor, + ) + result = await self._step( + agent_state=agent_state, + input_messages=input_messages, + max_steps=max_steps, + run_id=run_id, + request_start_timestamp_ns=request_start_timestamp_ns, + dry_run=dry_run, + ) + + # If dry run, return the request payload directly + if dry_run: + return result + + _, new_in_context_messages, stop_reason, usage = result + return _create_letta_response( + new_in_context_messages=new_in_context_messages, + use_assistant_message=use_assistant_message, + stop_reason=stop_reason, + usage=usage, + include_return_message_types=include_return_message_types, + ) + + @trace_method + async def step_stream_no_tokens( + self, + input_messages: list[MessageCreateBase], + max_steps: int = DEFAULT_MAX_STEPS, + use_assistant_message: bool = True, + request_start_timestamp_ns: int | None = None, + include_return_message_types: list[MessageType] | None = None, + run_id: str | None = None, + ): + agent_state = await self.agent_manager.get_agent_by_id_async( + agent_id=self.agent_id, + include_relationships=["tools", "memory", "tool_exec_environment_variables", "sources"], + actor=self.actor, + ) + current_in_context_messages, new_in_context_messages = await _prepare_in_context_messages_no_persist_async( + input_messages, agent_state, self.message_manager, self.actor + ) + initial_messages = new_in_context_messages + in_context_messages = current_in_context_messages + tool_rules_solver = ToolRulesSolver(agent_state.tool_rules) + llm_client = LLMClient.create( + provider_type=agent_state.llm_config.model_endpoint_type, + put_inner_thoughts_first=True, + actor=self.actor, + ) + stop_reason = None + job_update_metadata = None + usage = LettaUsageStatistics() + + # span for request + request_span = tracer.start_span("time_to_first_token", start_time=request_start_timestamp_ns) + request_span.set_attributes({f"llm_config.{k}": v for k, v in agent_state.llm_config.model_dump().items() if v is not None}) + + for i in range(max_steps): + if in_context_messages[-1].role == "approval": + approval_request_message = in_context_messages[-1] + step_metrics = await self.step_manager.get_step_metrics_async(step_id=approval_request_message.step_id, actor=self.actor) + persisted_messages, should_continue, stop_reason = await self._handle_ai_response( + approval_request_message.tool_calls[0], + [], # TODO: update this + agent_state, + tool_rules_solver, + usage, + reasoning_content=approval_request_message.content, + step_id=approval_request_message.step_id, + initial_messages=initial_messages, + is_final_step=(i == max_steps - 1), + step_metrics=step_metrics, + run_id=self.current_run_id, + is_approval=input_messages[0].approve, + is_denial=input_messages[0].approve == False, + denial_reason=input_messages[0].reason, + ) + new_message_idx = len(initial_messages) if initial_messages else 0 + self.response_messages.extend(persisted_messages[new_message_idx:]) + new_in_context_messages.extend(persisted_messages[new_message_idx:]) + initial_messages = None + in_context_messages = current_in_context_messages + new_in_context_messages + + # stream step + # TODO: improve TTFT + filter_user_messages = [m for m in persisted_messages if m.role != "user" and m.role != "approval"] + letta_messages = Message.to_letta_messages_from_list( + filter_user_messages, use_assistant_message=use_assistant_message, reverse=False + ) + + for message in letta_messages: + if include_return_message_types is None or message.message_type in include_return_message_types: + yield f"data: {message.model_dump_json()}\n\n" + else: + # Check for job cancellation at the start of each step + if await self._check_run_cancellation(): + stop_reason = LettaStopReason(stop_reason=StopReasonType.cancelled.value) + logger.info(f"Agent execution cancelled for run {self.current_run_id}") + yield f"data: {stop_reason.model_dump_json()}\n\n" + break + + step_id = generate_step_id() + step_start = get_utc_timestamp_ns() + agent_step_span = tracer.start_span("agent_step", start_time=step_start) + agent_step_span.set_attributes({"step_id": step_id}) + + step_progression = StepProgression.START + caught_exception = None + should_continue = False + step_metrics = StepMetrics(id=step_id) # Initialize metrics tracking + + # Create step early with PENDING status + logged_step = await self.step_manager.log_step_async( + actor=self.actor, + agent_id=agent_state.id, + provider_name=agent_state.llm_config.model_endpoint_type, + provider_category=agent_state.llm_config.provider_category or "base", + model=agent_state.llm_config.model, + model_endpoint=agent_state.llm_config.model_endpoint, + context_window_limit=agent_state.llm_config.context_window, + usage=UsageStatistics(completion_tokens=0, prompt_tokens=0, total_tokens=0), + provider_id=None, + run_id=self.current_run_id if self.current_run_id else None, + step_id=step_id, + project_id=agent_state.project_id, + status=StepStatus.PENDING, + model_handle=agent_state.llm_config.handle, + ) + # Only use step_id in messages if step was actually created + effective_step_id = step_id if logged_step else None + + try: + ( + request_data, + response_data, + current_in_context_messages, + new_in_context_messages, + valid_tool_names, + ) = await self._build_and_request_from_llm( + current_in_context_messages, + new_in_context_messages, + agent_state, + llm_client, + tool_rules_solver, + agent_step_span, + step_metrics, + run_id=run_id, + ) + in_context_messages = current_in_context_messages + new_in_context_messages + + step_progression = StepProgression.RESPONSE_RECEIVED + log_event("agent.stream_no_tokens.llm_response.received") # [3^] + + try: + response = await llm_client.convert_response_to_chat_completion( + response_data, in_context_messages, agent_state.llm_config + ) + except ValueError as e: + stop_reason = LettaStopReason(stop_reason=StopReasonType.invalid_llm_response.value) + raise e + + # update usage + usage.step_count += 1 + usage.completion_tokens += response.usage.completion_tokens + usage.prompt_tokens += response.usage.prompt_tokens + usage.total_tokens += response.usage.total_tokens + MetricRegistry().message_output_tokens.record( + response.usage.completion_tokens, dict(get_ctx_attributes(), **{"model.name": agent_state.llm_config.model}) + ) + + if not response.choices[0].message.tool_calls: + stop_reason = LettaStopReason(stop_reason=StopReasonType.no_tool_call.value) + raise ValueError("No tool calls found in response, model must make a tool call") + tool_call = response.choices[0].message.tool_calls[0] + if response.choices[0].message.reasoning_content: + reasoning = [ + ReasoningContent( + reasoning=response.choices[0].message.reasoning_content, + is_native=True, + signature=response.choices[0].message.reasoning_content_signature, + ) + ] + elif response.choices[0].message.omitted_reasoning_content: + reasoning = [OmittedReasoningContent()] + elif response.choices[0].message.content: + # Carry thought_signature on TextContent when ReasoningContent doesn't exist to hold it + reasoning = [ + TextContent( + text=response.choices[0].message.content, + signature=response.choices[0].message.reasoning_content_signature, + ) + ] # reasoning placed into content for legacy reasons + else: + # Preserve thought_signature even when there's no reasoning text. + # Gemini requires the signature on all function call parts in history; + # dropping it causes 400 INVALID_ARGUMENT on the next request. + sig = response.choices[0].message.reasoning_content_signature + if sig: + reasoning = [TextContent(text="", signature=sig)] + else: + self.logger.info("No reasoning content found.") + reasoning = None + + persisted_messages, should_continue, stop_reason = await self._handle_ai_response( + tool_call, + valid_tool_names, + agent_state, + tool_rules_solver, + response.usage, + reasoning_content=reasoning, + step_id=effective_step_id, + initial_messages=initial_messages, + agent_step_span=agent_step_span, + is_final_step=(i == max_steps - 1), + step_metrics=step_metrics, + ) + step_progression = StepProgression.STEP_LOGGED + + # Update step with actual usage now that we have it (if step was created) + if logged_step: + await self.step_manager.update_step_success_async(self.actor, step_id, response.usage, stop_reason) + + # TODO (cliandy): handle message contexts with larger refactor and dedupe logic + new_message_idx = len(initial_messages) if initial_messages else 0 + self.response_messages.extend(persisted_messages[new_message_idx:]) + new_in_context_messages.extend(persisted_messages[new_message_idx:]) + initial_messages = None + log_event("agent.stream_no_tokens.llm_response.processed") # [4^] + + # log step time + now = get_utc_timestamp_ns() + step_ns = now - step_start + agent_step_span.add_event(name="step_ms", attributes={"duration_ms": ns_to_ms(step_ns)}) + agent_step_span.end() + + # stream step + # TODO: improve TTFT + filter_user_messages = [m for m in persisted_messages if m.role != "user"] + letta_messages = Message.to_letta_messages_from_list( + filter_user_messages, use_assistant_message=use_assistant_message, reverse=False + ) + letta_messages = [m for m in letta_messages if m.message_type != "approval_response_message"] + + for message in letta_messages: + if include_return_message_types is None or message.message_type in include_return_message_types: + yield f"data: {message.model_dump_json()}\n\n" + + MetricRegistry().step_execution_time_ms_histogram.record(get_utc_timestamp_ns() - step_start, get_ctx_attributes()) + step_progression = StepProgression.FINISHED + + # Record step metrics for successful completion + if logged_step and step_metrics: + # Set the step_ns that was already calculated + step_metrics.step_ns = step_ns + await self._record_step_metrics( + step_id=step_id, + agent_state=agent_state, + step_metrics=step_metrics, + ) + + except Exception as e: + caught_exception = e + # Handle any unexpected errors during step processing + self.logger.error(f"Error during step processing: {e}") + job_update_metadata = {"error": str(e)} + + # This indicates we failed after we decided to stop stepping, which indicates a bug with our flow. + if not stop_reason: + stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + elif stop_reason.stop_reason in (StopReasonType.end_turn, StopReasonType.max_steps, StopReasonType.tool_rule): + self.logger.error("Error occurred during step processing, with valid stop reason: %s", stop_reason.stop_reason) + elif stop_reason.stop_reason not in ( + StopReasonType.no_tool_call, + StopReasonType.invalid_tool_call, + StopReasonType.invalid_llm_response, + ): + self.logger.error("Error occurred during step processing, with unexpected stop reason: %s", stop_reason.stop_reason) + + # Send error stop reason to client and re-raise + yield f"data: {stop_reason.model_dump_json()}\n\n", 500 + raise + + # Update step if it needs to be updated + finally: + if step_progression == StepProgression.FINISHED and should_continue: + continue + + self.logger.debug("Running cleanup for agent loop run: %s", self.current_run_id) + self.logger.info("Running final update. Step Progression: %s", step_progression) + try: + if step_progression == StepProgression.FINISHED and not should_continue: + # Successfully completed - update with final usage and stop reason + if stop_reason is None: + stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + # Note: step already updated with success status after _handle_ai_response + if logged_step: + await self.step_manager.update_step_stop_reason(self.actor, step_id, stop_reason.stop_reason) + break + + # Handle error cases + if step_progression < StepProgression.STEP_LOGGED: + # Error occurred before step was fully logged + import traceback + + if logged_step: + await self.step_manager.update_step_error_async( + actor=self.actor, + step_id=step_id, # Use original step_id for telemetry + error_type=type(caught_exception).__name__ if caught_exception is not None else "Unknown", + error_message=str(caught_exception) if caught_exception is not None else "Unknown error", + error_traceback=traceback.format_exc(), + stop_reason=stop_reason, + ) + + if step_progression <= StepProgression.RESPONSE_RECEIVED: + # TODO (cliandy): persist response if we get it back + if settings.track_errored_messages and initial_messages: + for message in initial_messages: + message.is_err = True + message.step_id = effective_step_id + await self.message_manager.create_many_messages_async( + initial_messages, + actor=self.actor, + project_id=agent_state.project_id, + template_id=agent_state.template_id, + ) + elif step_progression <= StepProgression.LOGGED_TRACE: + if stop_reason is None: + self.logger.error("Error in step after logging step") + stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + if logged_step: + await self.step_manager.update_step_stop_reason(self.actor, step_id, stop_reason.stop_reason) + else: + self.logger.error("Invalid StepProgression value") + + if settings.track_stop_reason: + await self._log_request(request_start_timestamp_ns, request_span, job_update_metadata, is_error=True) + + # Record partial step metrics on failure (capture whatever timing data we have) + if logged_step and step_metrics and step_progression < StepProgression.FINISHED: + # Calculate total step time up to the failure point + step_metrics.step_ns = get_utc_timestamp_ns() - step_start + await self._record_step_metrics( + step_id=step_id, + agent_state=agent_state, + step_metrics=step_metrics, + job_id=locals().get("run_id", self.current_run_id), + ) + + except Exception as e: + self.logger.error("Failed to update step: %s", e) + + if not should_continue: + break + + # Extend the in context message ids + if not agent_state.message_buffer_autoclear: + await self._rebuild_context_window( + in_context_messages=current_in_context_messages, + new_letta_messages=new_in_context_messages, + llm_config=agent_state.llm_config, + total_tokens=usage.total_tokens, + force=False, + run_id=run_id, + ) + + await self._log_request(request_start_timestamp_ns, request_span, job_update_metadata, is_error=False) + + # Return back usage + for finish_chunk in self.get_finish_chunks_for_stream(usage, stop_reason): + yield f"data: {finish_chunk}\n\n" + + async def _step( + self, + agent_state: AgentState, + input_messages: list[MessageCreateBase], + max_steps: int = DEFAULT_MAX_STEPS, + run_id: str | None = None, + request_start_timestamp_ns: int | None = None, + dry_run: bool = False, + ) -> Union[tuple[list[Message], list[Message], LettaStopReason | None, LettaUsageStatistics], dict]: + """ + Carries out an invocation of the agent loop. In each step, the agent + 1. Rebuilds its memory + 2. Generates a request for the LLM + 3. Fetches a response from the LLM + 4. Processes the response + """ + current_in_context_messages, new_in_context_messages = await _prepare_in_context_messages_no_persist_async( + input_messages, agent_state, self.message_manager, self.actor + ) + initial_messages = new_in_context_messages + in_context_messages = current_in_context_messages + tool_rules_solver = ToolRulesSolver(agent_state.tool_rules) + llm_client = LLMClient.create( + provider_type=agent_state.llm_config.model_endpoint_type, + put_inner_thoughts_first=True, + actor=self.actor, + ) + + # span for request + request_span = tracer.start_span("time_to_first_token") + request_span.set_attributes({f"llm_config.{k}": v for k, v in agent_state.llm_config.model_dump().items() if v is not None}) + + stop_reason = None + job_update_metadata = None + usage = LettaUsageStatistics() + for i in range(max_steps): + if in_context_messages[-1].role == "approval": + approval_request_message = in_context_messages[-1] + step_metrics = await self.step_manager.get_step_metrics_async(step_id=approval_request_message.step_id, actor=self.actor) + persisted_messages, should_continue, stop_reason = await self._handle_ai_response( + approval_request_message.tool_calls[0], + [], # TODO: update this + agent_state, + tool_rules_solver, + usage, + reasoning_content=approval_request_message.content, + step_id=approval_request_message.step_id, + initial_messages=initial_messages, + is_final_step=(i == max_steps - 1), + step_metrics=step_metrics, + run_id=run_id or self.current_run_id, + is_approval=input_messages[0].approve, + is_denial=input_messages[0].approve == False, + denial_reason=input_messages[0].reason, + ) + new_message_idx = len(initial_messages) if initial_messages else 0 + self.response_messages.extend(persisted_messages[new_message_idx:]) + new_in_context_messages.extend(persisted_messages[new_message_idx:]) + initial_messages = None + in_context_messages = current_in_context_messages + new_in_context_messages + else: + # If dry run, build request data and return it without making LLM call + if dry_run: + request_data, valid_tool_names = await self._create_llm_request_data_async( + llm_client=llm_client, + in_context_messages=current_in_context_messages + new_in_context_messages, + agent_state=agent_state, + tool_rules_solver=tool_rules_solver, + ) + return request_data + + # Check for job cancellation at the start of each step + if await self._check_run_cancellation(): + stop_reason = LettaStopReason(stop_reason=StopReasonType.cancelled.value) + logger.info(f"Agent execution cancelled for run {self.current_run_id}") + break + + step_id = generate_step_id() + step_start = get_utc_timestamp_ns() + agent_step_span = tracer.start_span("agent_step", start_time=step_start) + agent_step_span.set_attributes({"step_id": step_id}) + + step_progression = StepProgression.START + caught_exception = None + should_continue = False + step_metrics = StepMetrics(id=step_id) # Initialize metrics tracking + + # Create step early with PENDING status + logged_step = await self.step_manager.log_step_async( + actor=self.actor, + agent_id=agent_state.id, + provider_name=agent_state.llm_config.model_endpoint_type, + provider_category=agent_state.llm_config.provider_category or "base", + model=agent_state.llm_config.model, + model_endpoint=agent_state.llm_config.model_endpoint, + context_window_limit=agent_state.llm_config.context_window, + usage=UsageStatistics(completion_tokens=0, prompt_tokens=0, total_tokens=0), + provider_id=None, + run_id=run_id if run_id else self.current_run_id, + step_id=step_id, + project_id=agent_state.project_id, + status=StepStatus.PENDING, + model_handle=agent_state.llm_config.handle, + ) + # Only use step_id in messages if step was actually created + effective_step_id = step_id if logged_step else None + + try: + ( + request_data, + response_data, + current_in_context_messages, + new_in_context_messages, + valid_tool_names, + ) = await self._build_and_request_from_llm( + current_in_context_messages, + new_in_context_messages, + agent_state, + llm_client, + tool_rules_solver, + agent_step_span, + step_metrics, + run_id=run_id, + ) + in_context_messages = current_in_context_messages + new_in_context_messages + + step_progression = StepProgression.RESPONSE_RECEIVED + log_event("agent.step.llm_response.received") # [3^] + + try: + response = await llm_client.convert_response_to_chat_completion( + response_data, in_context_messages, agent_state.llm_config + ) + except ValueError as e: + stop_reason = LettaStopReason(stop_reason=StopReasonType.invalid_llm_response.value) + raise e + + usage.step_count += 1 + usage.completion_tokens += response.usage.completion_tokens + usage.prompt_tokens += response.usage.prompt_tokens + usage.total_tokens += response.usage.total_tokens + usage.run_ids = [run_id] if run_id else None + MetricRegistry().message_output_tokens.record( + response.usage.completion_tokens, dict(get_ctx_attributes(), **{"model.name": agent_state.llm_config.model}) + ) + + if not response.choices[0].message.tool_calls: + stop_reason = LettaStopReason(stop_reason=StopReasonType.no_tool_call.value) + raise ValueError("No tool calls found in response, model must make a tool call") + tool_call = response.choices[0].message.tool_calls[0] + if response.choices[0].message.reasoning_content: + reasoning = [ + ReasoningContent( + reasoning=response.choices[0].message.reasoning_content, + is_native=True, + signature=response.choices[0].message.reasoning_content_signature, + ) + ] + elif response.choices[0].message.content: + # Carry thought_signature on TextContent when ReasoningContent doesn't exist to hold it + reasoning = [ + TextContent( + text=response.choices[0].message.content, + signature=response.choices[0].message.reasoning_content_signature, + ) + ] # reasoning placed into content for legacy reasons + elif response.choices[0].message.omitted_reasoning_content: + reasoning = [OmittedReasoningContent()] + else: + # Preserve thought_signature even when there's no reasoning text. + # Gemini requires the signature on all function call parts in history; + # dropping it causes 400 INVALID_ARGUMENT on the next request. + sig = response.choices[0].message.reasoning_content_signature + if sig: + reasoning = [TextContent(text="", signature=sig)] + else: + self.logger.info("No reasoning content found.") + reasoning = None + + persisted_messages, should_continue, stop_reason = await self._handle_ai_response( + tool_call, + valid_tool_names, + agent_state, + tool_rules_solver, + response.usage, + reasoning_content=reasoning, + step_id=effective_step_id, + initial_messages=initial_messages, + agent_step_span=agent_step_span, + is_final_step=(i == max_steps - 1), + run_id=run_id, + step_metrics=step_metrics, + ) + step_progression = StepProgression.STEP_LOGGED + + # Update step with actual usage now that we have it (if step was created) + if logged_step: + await self.step_manager.update_step_success_async(self.actor, step_id, response.usage, stop_reason) + + new_message_idx = len(initial_messages) if initial_messages else 0 + self.response_messages.extend(persisted_messages[new_message_idx:]) + new_in_context_messages.extend(persisted_messages[new_message_idx:]) + + initial_messages = None + log_event("agent.step.llm_response.processed") # [4^] + + # log step time + now = get_utc_timestamp_ns() + step_ns = now - step_start + agent_step_span.add_event(name="step_ms", attributes={"duration_ms": ns_to_ms(step_ns)}) + agent_step_span.end() + + MetricRegistry().step_execution_time_ms_histogram.record(get_utc_timestamp_ns() - step_start, get_ctx_attributes()) + step_progression = StepProgression.FINISHED + + # Record step metrics for successful completion + if logged_step and step_metrics: + # Set the step_ns that was already calculated + step_metrics.step_ns = step_ns + await self._record_step_metrics( + step_id=step_id, + agent_state=agent_state, + step_metrics=step_metrics, + run_id=run_id if run_id else self.current_run_id, + ) + + except Exception as e: + caught_exception = e + # Handle any unexpected errors during step processing + self.logger.error(f"Error during step processing: {e}") + job_update_metadata = {"error": str(e)} + + # This indicates we failed after we decided to stop stepping, which indicates a bug with our flow. + if not stop_reason: + stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + elif stop_reason.stop_reason in (StopReasonType.end_turn, StopReasonType.max_steps, StopReasonType.tool_rule): + self.logger.error("Error occurred during step processing, with valid stop reason: %s", stop_reason.stop_reason) + elif stop_reason.stop_reason not in ( + StopReasonType.no_tool_call, + StopReasonType.invalid_tool_call, + StopReasonType.invalid_llm_response, + ): + self.logger.error("Error occurred during step processing, with unexpected stop reason: %s", stop_reason.stop_reason) + raise + + # Update step if it needs to be updated + finally: + if step_progression == StepProgression.FINISHED and should_continue: + continue + + self.logger.debug("Running cleanup for agent loop run: %s", self.current_run_id) + self.logger.info("Running final update. Step Progression: %s", step_progression) + try: + if step_progression == StepProgression.FINISHED and not should_continue: + # Successfully completed - update with final usage and stop reason + if stop_reason is None: + stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + if logged_step: + await self.step_manager.update_step_success_async(self.actor, step_id, usage, stop_reason) + break + + # Handle error cases + if step_progression < StepProgression.STEP_LOGGED: + # Error occurred before step was fully logged + import traceback + + if logged_step: + await self.step_manager.update_step_error_async( + actor=self.actor, + step_id=step_id, # Use original step_id for telemetry + error_type=type(caught_exception).__name__ if caught_exception is not None else "Unknown", + error_message=str(caught_exception) if caught_exception is not None else "Unknown error", + error_traceback=traceback.format_exc(), + stop_reason=stop_reason, + ) + + if step_progression <= StepProgression.RESPONSE_RECEIVED: + # TODO (cliandy): persist response if we get it back + if settings.track_errored_messages and initial_messages: + for message in initial_messages: + message.is_err = True + message.step_id = effective_step_id + await self.message_manager.create_many_messages_async( + initial_messages, + actor=self.actor, + project_id=agent_state.project_id, + template_id=agent_state.template_id, + ) + elif step_progression <= StepProgression.LOGGED_TRACE: + if stop_reason is None: + self.logger.error("Error in step after logging step") + stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + if logged_step: + await self.step_manager.update_step_stop_reason(self.actor, step_id, stop_reason.stop_reason) + else: + self.logger.error("Invalid StepProgression value") + + if settings.track_stop_reason: + await self._log_request(request_start_timestamp_ns, request_span, job_update_metadata, is_error=True) + + # Record partial step metrics on failure (capture whatever timing data we have) + if logged_step and step_metrics and step_progression < StepProgression.FINISHED: + # Calculate total step time up to the failure point + step_metrics.step_ns = get_utc_timestamp_ns() - step_start + await self._record_step_metrics( + step_id=step_id, + agent_state=agent_state, + step_metrics=step_metrics, + job_id=locals().get("run_id", self.current_run_id), + ) + + except Exception as e: + self.logger.error("Failed to update step: %s", e) + + if not should_continue: + break + + # Extend the in context message ids + if not agent_state.message_buffer_autoclear: + await self._rebuild_context_window( + in_context_messages=current_in_context_messages, + new_letta_messages=new_in_context_messages, + llm_config=agent_state.llm_config, + total_tokens=usage.total_tokens, + force=False, + run_id=run_id, + ) + + await self._log_request(request_start_timestamp_ns, request_span, job_update_metadata, is_error=False) + + return current_in_context_messages, new_in_context_messages, stop_reason, usage + + async def _update_agent_last_run_metrics(self, completion_time: datetime, duration_ms: float) -> None: + if not settings.track_last_agent_run: + return + try: + await self.agent_manager.update_agent_async( + agent_id=self.agent_id, + agent_update=UpdateAgent(last_run_completion=completion_time, last_run_duration_ms=duration_ms), + actor=self.actor, + ) + except Exception as e: + self.logger.error(f"Failed to update agent's last run metrics: {e}") + + @trace_method + async def step_stream( + self, + input_messages: list[MessageCreateBase], + max_steps: int = DEFAULT_MAX_STEPS, + use_assistant_message: bool = True, + request_start_timestamp_ns: int | None = None, + include_return_message_types: list[MessageType] | None = None, + run_id: str | None = None, + ) -> AsyncGenerator[str, None]: + """ + Carries out an invocation of the agent loop in a streaming fashion that yields partial tokens. + Whenever we detect a tool call, we yield from _handle_ai_response as well. At each step, the agent + 1. Rebuilds its memory + 2. Generates a request for the LLM + 3. Fetches a response from the LLM + 4. Processes the response + """ + agent_state = await self.agent_manager.get_agent_by_id_async( + agent_id=self.agent_id, + include_relationships=["tools", "memory", "tool_exec_environment_variables", "sources"], + actor=self.actor, + ) + current_in_context_messages, new_in_context_messages = await _prepare_in_context_messages_no_persist_async( + input_messages, agent_state, self.message_manager, self.actor + ) + initial_messages = new_in_context_messages + in_context_messages = current_in_context_messages + + tool_rules_solver = ToolRulesSolver(agent_state.tool_rules) + llm_client = LLMClient.create( + provider_type=agent_state.llm_config.model_endpoint_type, + put_inner_thoughts_first=True, + actor=self.actor, + ) + stop_reason = None + job_update_metadata = None + usage = LettaUsageStatistics() + first_chunk, request_span = True, None + if request_start_timestamp_ns: + request_span = tracer.start_span("time_to_first_token", start_time=request_start_timestamp_ns) + request_span.set_attributes({f"llm_config.{k}": v for k, v in agent_state.llm_config.model_dump().items() if v is not None}) + + for i in range(max_steps): + if in_context_messages[-1].role == "approval": + approval_request_message = in_context_messages[-1] + step_metrics = await self.step_manager.get_step_metrics_async(step_id=approval_request_message.step_id, actor=self.actor) + persisted_messages, should_continue, stop_reason = await self._handle_ai_response( + approval_request_message.tool_calls[0], + [], # TODO: update this + agent_state, + tool_rules_solver, + usage, + reasoning_content=approval_request_message.content, + step_id=approval_request_message.step_id, + initial_messages=new_in_context_messages, + is_final_step=(i == max_steps - 1), + step_metrics=step_metrics, + run_id=self.current_run_id, + is_approval=input_messages[0].approve, + is_denial=input_messages[0].approve == False, + denial_reason=input_messages[0].reason, + ) + new_message_idx = len(initial_messages) if initial_messages else 0 + self.response_messages.extend(persisted_messages[new_message_idx:]) + new_in_context_messages.extend(persisted_messages[new_message_idx:]) + initial_messages = None + in_context_messages = current_in_context_messages + new_in_context_messages + + # yields tool response as this is handled from Letta and not the response from the LLM provider + tool_return = [msg for msg in persisted_messages if msg.role == "tool"][-1].to_letta_messages()[0] + if not (use_assistant_message and tool_return.name == "send_message"): + # Apply message type filtering if specified + if include_return_message_types is None or tool_return.message_type in include_return_message_types: + yield f"data: {tool_return.model_dump_json()}\n\n" + else: + step_id = generate_step_id() + # Check for job cancellation at the start of each step + if await self._check_run_cancellation(): + stop_reason = LettaStopReason(stop_reason=StopReasonType.cancelled.value) + logger.info(f"Agent execution cancelled for run {self.current_run_id}") + yield f"data: {stop_reason.model_dump_json()}\n\n" + break + + step_start = get_utc_timestamp_ns() + agent_step_span = tracer.start_span("agent_step", start_time=step_start) + agent_step_span.set_attributes({"step_id": step_id}) + + step_progression = StepProgression.START + caught_exception = None + should_continue = False + step_metrics = StepMetrics(id=step_id) # Initialize metrics tracking + + # Create step early with PENDING status + logged_step = await self.step_manager.log_step_async( + actor=self.actor, + agent_id=agent_state.id, + provider_name=agent_state.llm_config.model_endpoint_type, + provider_category=agent_state.llm_config.provider_category or "base", + model=agent_state.llm_config.model, + model_endpoint=agent_state.llm_config.model_endpoint, + context_window_limit=agent_state.llm_config.context_window, + usage=UsageStatistics(completion_tokens=0, prompt_tokens=0, total_tokens=0), + provider_id=None, + run_id=self.current_run_id if self.current_run_id else None, + step_id=step_id, + project_id=agent_state.project_id, + status=StepStatus.PENDING, + model_handle=agent_state.llm_config.handle, + ) + # Only use step_id in messages if step was actually created + effective_step_id = step_id if logged_step else None + + try: + ( + request_data, + stream, + current_in_context_messages, + new_in_context_messages, + valid_tool_names, + provider_request_start_timestamp_ns, + ) = await self._build_and_request_from_llm_streaming( + first_chunk, + agent_step_span, + request_start_timestamp_ns, + current_in_context_messages, + new_in_context_messages, + agent_state, + llm_client, + tool_rules_solver, + run_id=run_id, + step_id=step_id, + ) + + step_progression = StepProgression.STREAM_RECEIVED + log_event("agent.stream.llm_response.received") # [3^] + + # TODO: THIS IS INCREDIBLY UGLY + # TODO: THERE ARE MULTIPLE COPIES OF THE LLM_CONFIG EVERYWHERE THAT ARE GETTING MANIPULATED + if agent_state.llm_config.model_endpoint_type in [ProviderType.anthropic, ProviderType.bedrock]: + interface = AnthropicStreamingInterface( + use_assistant_message=use_assistant_message, + put_inner_thoughts_in_kwarg=agent_state.llm_config.put_inner_thoughts_in_kwargs, + requires_approval_tools=tool_rules_solver.get_requires_approval_tools(valid_tool_names), + ) + elif agent_state.llm_config.model_endpoint_type == ProviderType.openai: + interface = OpenAIStreamingInterface( + use_assistant_message=use_assistant_message, + is_openai_proxy=agent_state.llm_config.provider_name == "lmstudio_openai", + messages=current_in_context_messages + new_in_context_messages, + tools=request_data.get("tools", []), + put_inner_thoughts_in_kwarg=agent_state.llm_config.put_inner_thoughts_in_kwargs, + requires_approval_tools=tool_rules_solver.get_requires_approval_tools(valid_tool_names), + ) + else: + raise ValueError(f"Streaming not supported for {agent_state.llm_config}") + + async for chunk in interface.process( + stream, + ttft_span=request_span, + ): + # Measure TTFT (trace, metric, and db). This should be consolidated. + if first_chunk and request_span is not None: + now = get_utc_timestamp_ns() + ttft_ns = now - request_start_timestamp_ns + + request_span.add_event(name="time_to_first_token_ms", attributes={"ttft_ms": ns_to_ms(ttft_ns)}) + metric_attributes = get_ctx_attributes() + metric_attributes["model.name"] = agent_state.llm_config.model + MetricRegistry().ttft_ms_histogram.record(ns_to_ms(ttft_ns), metric_attributes) + + if self.current_run_id and self.job_manager: + await self.job_manager.record_ttft(self.current_run_id, ttft_ns, self.actor) + + first_chunk = False + + if include_return_message_types is None or chunk.message_type in include_return_message_types: + # filter down returned data + yield f"data: {chunk.model_dump_json()}\n\n" + + stream_end_time_ns = get_utc_timestamp_ns() + + # Some providers that rely on the OpenAI client currently e.g. LMStudio don't get usage metrics back on the last streaming chunk, fall back to manual values + if isinstance(interface, OpenAIStreamingInterface) and not interface.input_tokens and not interface.output_tokens: + logger.warning( + f"No token usage metrics received from OpenAI streaming interface for {agent_state.llm_config.model}, falling back to estimated values. Input tokens: {interface.fallback_input_tokens}, Output tokens: {interface.fallback_output_tokens}" + ) + interface.input_tokens = interface.fallback_input_tokens + interface.output_tokens = interface.fallback_output_tokens + + usage.step_count += 1 + usage.completion_tokens += interface.output_tokens + usage.prompt_tokens += interface.input_tokens + usage.total_tokens += interface.input_tokens + interface.output_tokens + # Aggregate cache and reasoning tokens if available from streaming interface (handle None defaults) + if hasattr(interface, "cached_tokens") and interface.cached_tokens is not None: + usage.cached_input_tokens = (usage.cached_input_tokens or 0) + interface.cached_tokens + if hasattr(interface, "cache_read_tokens") and interface.cache_read_tokens is not None: + usage.cached_input_tokens = (usage.cached_input_tokens or 0) + interface.cache_read_tokens + if hasattr(interface, "cache_creation_tokens") and interface.cache_creation_tokens is not None: + usage.cache_write_tokens = (usage.cache_write_tokens or 0) + interface.cache_creation_tokens + if hasattr(interface, "reasoning_tokens") and interface.reasoning_tokens is not None: + usage.reasoning_tokens = (usage.reasoning_tokens or 0) + interface.reasoning_tokens + MetricRegistry().message_output_tokens.record( + usage.completion_tokens, dict(get_ctx_attributes(), **{"model.name": agent_state.llm_config.model}) + ) + + # log LLM request time + llm_request_ns = stream_end_time_ns - provider_request_start_timestamp_ns + step_metrics.llm_request_ns = llm_request_ns + + llm_request_ms = ns_to_ms(llm_request_ns) + agent_step_span.add_event(name="llm_request_ms", attributes={"duration_ms": llm_request_ms}) + MetricRegistry().llm_execution_time_ms_histogram.record( + llm_request_ms, + dict(get_ctx_attributes(), **{"model.name": agent_state.llm_config.model}), + ) + + # Process resulting stream content + try: + tool_call = interface.get_tool_call_object() + except ValueError as e: + stop_reason = LettaStopReason(stop_reason=StopReasonType.no_tool_call.value) + raise e + except Exception as e: + stop_reason = LettaStopReason(stop_reason=StopReasonType.invalid_tool_call.value) + raise e + reasoning_content = interface.get_reasoning_content() + + # Log provider trace telemetry after stream processing + await llm_client.log_provider_trace_async( + request_data=request_data, + response_json={ + "content": { + "tool_call": tool_call.model_dump() if tool_call else None, + "reasoning": [c.model_dump() for c in reasoning_content] if reasoning_content else [], + }, + "model": getattr(interface, "model", None), + "usage": { + "input_tokens": interface.input_tokens, + "output_tokens": interface.output_tokens, + }, + }, + llm_config=agent_state.llm_config, + latency_ms=int(llm_request_ms), + ) + persisted_messages, should_continue, stop_reason = await self._handle_ai_response( + tool_call, + valid_tool_names, + agent_state, + tool_rules_solver, + UsageStatistics( + completion_tokens=usage.completion_tokens, + prompt_tokens=usage.prompt_tokens, + total_tokens=usage.total_tokens, + ), + reasoning_content=reasoning_content, + pre_computed_assistant_message_id=interface.letta_message_id, + step_id=effective_step_id, + initial_messages=initial_messages, + agent_step_span=agent_step_span, + is_final_step=(i == max_steps - 1), + step_metrics=step_metrics, + ) + step_progression = StepProgression.STEP_LOGGED + + # Update step with actual usage now that we have it (if step was created) + if logged_step: + # Build detailed token breakdowns from LettaUsageStatistics + # Use `is not None` to capture 0 values (meaning "provider reported 0 cached/reasoning tokens") + # Only include fields that were actually reported by the provider + prompt_details = None + if usage.cached_input_tokens is not None or usage.cache_write_tokens is not None: + prompt_details = UsageStatisticsPromptTokenDetails( + cached_tokens=usage.cached_input_tokens if usage.cached_input_tokens is not None else None, + cache_read_tokens=usage.cached_input_tokens if usage.cached_input_tokens is not None else None, + cache_creation_tokens=usage.cache_write_tokens if usage.cache_write_tokens is not None else None, + ) + + completion_details = None + if usage.reasoning_tokens is not None: + completion_details = UsageStatisticsCompletionTokenDetails( + reasoning_tokens=usage.reasoning_tokens, + ) + + await self.step_manager.update_step_success_async( + self.actor, + step_id, + UsageStatistics( + completion_tokens=usage.completion_tokens, + prompt_tokens=usage.prompt_tokens, + total_tokens=usage.total_tokens, + prompt_tokens_details=prompt_details, + completion_tokens_details=completion_details, + ), + stop_reason, + ) + + new_message_idx = len(initial_messages) if initial_messages else 0 + self.response_messages.extend(persisted_messages[new_message_idx:]) + new_in_context_messages.extend(persisted_messages[new_message_idx:]) + + initial_messages = None + + # log total step time + now = get_utc_timestamp_ns() + step_ns = now - step_start + agent_step_span.add_event(name="step_ms", attributes={"duration_ms": ns_to_ms(step_ns)}) + agent_step_span.end() + + # TODO (cliandy): the stream POST request span has ended at this point, we should tie this to the stream + # log_event("agent.stream.llm_response.processed") # [4^] + + if persisted_messages[-1].role != "approval": + # yields tool response as this is handled from Letta and not the response from the LLM provider + tool_return = [msg for msg in persisted_messages if msg.role == "tool"][-1].to_letta_messages()[0] + if not (use_assistant_message and tool_return.name == "send_message"): + # Apply message type filtering if specified + if include_return_message_types is None or tool_return.message_type in include_return_message_types: + yield f"data: {tool_return.model_dump_json()}\n\n" + + # TODO (cliandy): consolidate and expand with trace + MetricRegistry().step_execution_time_ms_histogram.record(get_utc_timestamp_ns() - step_start, get_ctx_attributes()) + step_progression = StepProgression.FINISHED + + # Record step metrics for successful completion + if logged_step and step_metrics: + try: + # Set the step_ns that was already calculated + step_metrics.step_ns = step_ns + + # Get context attributes for project and template IDs + ctx_attrs = get_ctx_attributes() + + await self._record_step_metrics( + step_id=step_id, + agent_state=agent_state, + step_metrics=step_metrics, + ctx_attrs=ctx_attrs, + job_id=self.current_run_id, + ) + except Exception as metrics_error: + self.logger.warning(f"Failed to record step metrics: {metrics_error}") + + except Exception as e: + caught_exception = e + # Handle any unexpected errors during step processing + self.logger.error(f"Error during step processing: {e}") + job_update_metadata = {"error": str(e)} + + # This indicates we failed after we decided to stop stepping, which indicates a bug with our flow. + if not stop_reason: + stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + elif stop_reason.stop_reason in (StopReasonType.end_turn, StopReasonType.max_steps, StopReasonType.tool_rule): + self.logger.error("Error occurred during step processing, with valid stop reason: %s", stop_reason.stop_reason) + elif stop_reason.stop_reason not in ( + StopReasonType.no_tool_call, + StopReasonType.invalid_tool_call, + StopReasonType.invalid_llm_response, + ): + self.logger.error("Error occurred during step processing, with unexpected stop reason: %s", stop_reason.stop_reason) + + # Send error stop reason to client and re-raise with expected response code + yield f"data: {stop_reason.model_dump_json()}\n\n", 500 + raise + + # Update step if it needs to be updated + finally: + if step_progression == StepProgression.FINISHED and should_continue: + continue + + self.logger.debug("Running cleanup for agent loop run: %s", self.current_run_id) + self.logger.info("Running final update. Step Progression: %s", step_progression) + try: + if step_progression == StepProgression.FINISHED and not should_continue: + # Successfully completed - update with final usage and stop reason + if stop_reason is None: + stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + # Note: step already updated with success status after _handle_ai_response + if logged_step: + await self.step_manager.update_step_stop_reason(self.actor, step_id, stop_reason.stop_reason) + break + + # Handle error cases + if step_progression < StepProgression.STEP_LOGGED: + # Error occurred before step was fully logged + import traceback + + if logged_step: + await self.step_manager.update_step_error_async( + actor=self.actor, + step_id=step_id, # Use original step_id for telemetry + error_type=type(caught_exception).__name__ if caught_exception is not None else "Unknown", + error_message=str(caught_exception) if caught_exception is not None else "Unknown error", + error_traceback=traceback.format_exc(), + stop_reason=stop_reason, + ) + + if step_progression <= StepProgression.STREAM_RECEIVED: + if first_chunk and settings.track_errored_messages and initial_messages: + for message in initial_messages: + message.is_err = True + message.step_id = effective_step_id + await self.message_manager.create_many_messages_async( + initial_messages, + actor=self.actor, + project_id=agent_state.project_id, + template_id=agent_state.template_id, + ) + elif step_progression <= StepProgression.LOGGED_TRACE: + if stop_reason is None: + self.logger.error("Error in step after logging step") + stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + if logged_step: + await self.step_manager.update_step_stop_reason(self.actor, step_id, stop_reason.stop_reason) + else: + self.logger.error("Invalid StepProgression value") + + # Do tracking for failure cases. Can consolidate with success conditions later. + if settings.track_stop_reason: + await self._log_request(request_start_timestamp_ns, request_span, job_update_metadata, is_error=True) + + # Record partial step metrics on failure (capture whatever timing data we have) + if logged_step and step_metrics and step_progression < StepProgression.FINISHED: + try: + # Calculate total step time up to the failure point + step_metrics.step_ns = get_utc_timestamp_ns() - step_start + + # Get context attributes for project and template IDs + ctx_attrs = get_ctx_attributes() + + await self._record_step_metrics( + step_id=step_id, + agent_state=agent_state, + step_metrics=step_metrics, + ctx_attrs=ctx_attrs, + job_id=locals().get("run_id", self.current_run_id), + ) + except Exception as metrics_error: + self.logger.warning(f"Failed to record step metrics: {metrics_error}") + + except Exception as e: + self.logger.error("Failed to update step: %s", e) + + if not should_continue: + break + # Extend the in context message ids + if not agent_state.message_buffer_autoclear: + await self._rebuild_context_window( + in_context_messages=current_in_context_messages, + new_letta_messages=new_in_context_messages, + llm_config=agent_state.llm_config, + total_tokens=usage.total_tokens, + force=False, + run_id=run_id, + ) + + await self._log_request(request_start_timestamp_ns, request_span, job_update_metadata, is_error=False) + + for finish_chunk in self.get_finish_chunks_for_stream(usage, stop_reason): + yield f"data: {finish_chunk}\n\n" + + async def _log_request( + self, request_start_timestamp_ns: int, request_span: "Span | None", job_update_metadata: dict | None, is_error: bool + ): + if request_start_timestamp_ns: + now_ns, now = get_utc_timestamp_ns(), get_utc_time() + duration_ns = now_ns - request_start_timestamp_ns + if request_span: + request_span.add_event(name="letta_request_ms", attributes={"duration_ms": ns_to_ms(duration_ns)}) + await self._update_agent_last_run_metrics(now, ns_to_ms(duration_ns)) + if settings.track_agent_run and self.current_run_id: + await self.job_manager.record_response_duration(self.current_run_id, duration_ns, self.actor) + await self.job_manager.safe_update_job_status_async( + job_id=self.current_run_id, + new_status=JobStatus.failed if is_error else JobStatus.completed, + actor=self.actor, + metadata=job_update_metadata, + ) + if request_span: + request_span.end() + + async def _record_step_metrics( + self, + *, + step_id: str, + agent_state: AgentState, + step_metrics: StepMetrics, + ctx_attrs: dict | None = None, + job_id: str | None = None, + ) -> None: + try: + attrs = ctx_attrs or get_ctx_attributes() + await self.step_manager.record_step_metrics_async( + actor=self.actor, + step_id=step_id, + llm_request_ns=step_metrics.llm_request_ns, + tool_execution_ns=step_metrics.tool_execution_ns, + step_ns=step_metrics.step_ns, + agent_id=agent_state.id, + job_id=job_id or self.current_run_id, + project_id=attrs.get("project.id") or agent_state.project_id, + template_id=attrs.get("template.id"), + base_template_id=attrs.get("base_template.id"), + ) + except Exception as metrics_error: + self.logger.warning(f"Failed to record step metrics: {metrics_error}") + + # noinspection PyInconsistentReturns + async def _build_and_request_from_llm( + self, + current_in_context_messages: list[Message], + new_in_context_messages: list[Message], + agent_state: AgentState, + llm_client: LLMClientBase, + tool_rules_solver: ToolRulesSolver, + agent_step_span: "Span", + step_metrics: StepMetrics, + run_id: str | None = None, + ) -> tuple[dict, dict, list[Message], list[Message], list[str]] | None: + for attempt in range(self.max_summarization_retries + 1): + try: + log_event("agent.stream_no_tokens.messages.refreshed") + # Create LLM request data + request_data, valid_tool_names = await self._create_llm_request_data_async( + llm_client=llm_client, + in_context_messages=current_in_context_messages + new_in_context_messages, + agent_state=agent_state, + tool_rules_solver=tool_rules_solver, + ) + log_event("agent.stream_no_tokens.llm_request.created") + + async with AsyncTimer() as timer: + # Attempt LLM request with telemetry + llm_client.set_telemetry_context( + telemetry_manager=self.telemetry_manager, + agent_id=self.agent_id, + agent_tags=agent_state.tags, + run_id=self.current_run_id, + step_id=step_metrics.id, + call_type=LLMCallType.agent_step, + ) + response = await llm_client.request_async_with_telemetry(request_data, agent_state.llm_config) + + # Track LLM request time + step_metrics.llm_request_ns = int(timer.elapsed_ns) + + MetricRegistry().llm_execution_time_ms_histogram.record( + timer.elapsed_ms, + dict(get_ctx_attributes(), **{"model.name": agent_state.llm_config.model}), + ) + agent_step_span.add_event(name="llm_request_ms", attributes={"duration_ms": timer.elapsed_ms}) + + return request_data, response, current_in_context_messages, new_in_context_messages, valid_tool_names + + except Exception as e: + if attempt == self.max_summarization_retries: + raise e + + # Handle the error and prepare for retry + current_in_context_messages = await self._handle_llm_error( + e, + llm_client=llm_client, + in_context_messages=current_in_context_messages, + new_letta_messages=new_in_context_messages, + llm_config=agent_state.llm_config, + force=True, + run_id=run_id, + ) + new_in_context_messages = [] + log_event(f"agent.stream_no_tokens.retry_attempt.{attempt + 1}") + + # noinspection PyInconsistentReturns + async def _build_and_request_from_llm_streaming( + self, + first_chunk: bool, + ttft_span: "Span", + request_start_timestamp_ns: int, + current_in_context_messages: list[Message], + new_in_context_messages: list[Message], + agent_state: AgentState, + llm_client: LLMClientBase, + tool_rules_solver: ToolRulesSolver, + run_id: str | None = None, + step_id: str | None = None, + ) -> tuple[dict, AsyncStream[ChatCompletionChunk], list[Message], list[Message], list[str], int] | None: + for attempt in range(self.max_summarization_retries + 1): + try: + log_event("agent.stream_no_tokens.messages.refreshed") + # Create LLM request data + request_data, valid_tool_names = await self._create_llm_request_data_async( + llm_client=llm_client, + in_context_messages=current_in_context_messages + new_in_context_messages, + agent_state=agent_state, + tool_rules_solver=tool_rules_solver, + ) + log_event("agent.stream.llm_request.created") # [2^] + + provider_request_start_timestamp_ns = get_utc_timestamp_ns() + if first_chunk and ttft_span is not None: + request_start_to_provider_request_start_ns = provider_request_start_timestamp_ns - request_start_timestamp_ns + ttft_span.add_event( + name="request_start_to_provider_request_start_ns", + attributes={"request_start_to_provider_request_start_ns": ns_to_ms(request_start_to_provider_request_start_ns)}, + ) + + # Set telemetry context before streaming + llm_client.set_telemetry_context( + telemetry_manager=self.telemetry_manager, + agent_id=self.agent_id, + agent_tags=agent_state.tags, + run_id=self.current_run_id, + step_id=step_id, + call_type=LLMCallType.agent_step, + ) + + # Attempt LLM request with telemetry wrapper + return ( + request_data, + await llm_client.stream_async(request_data, agent_state.llm_config), + current_in_context_messages, + new_in_context_messages, + valid_tool_names, + provider_request_start_timestamp_ns, + ) + + except Exception as e: + if attempt == self.max_summarization_retries: + raise e + + # Handle the error and prepare for retry + current_in_context_messages = await self._handle_llm_error( + e, + llm_client=llm_client, + in_context_messages=current_in_context_messages, + new_letta_messages=new_in_context_messages, + llm_config=agent_state.llm_config, + force=True, + run_id=run_id, + ) + new_in_context_messages: list[Message] = [] + log_event(f"agent.stream_no_tokens.retry_attempt.{attempt + 1}") + + @trace_method + async def _handle_llm_error( + self, + e: Exception, + llm_client: LLMClientBase, + in_context_messages: list[Message], + new_letta_messages: list[Message], + llm_config: LLMConfig, + force: bool, + run_id: str | None = None, + step_id: str | None = None, + ) -> list[Message]: + if isinstance(e, ContextWindowExceededError): + return await self._rebuild_context_window( + in_context_messages=in_context_messages, + new_letta_messages=new_letta_messages, + llm_config=llm_config, + force=force, + run_id=run_id, + step_id=step_id, + ) + elif isinstance(e, LLMError): + raise + else: + raise llm_client.handle_llm_error(e, llm_config=llm_config) + + @trace_method + async def _rebuild_context_window( + self, + in_context_messages: list[Message], + new_letta_messages: list[Message], + llm_config: LLMConfig, + total_tokens: int | None = None, + force: bool = False, + run_id: str | None = None, + step_id: str | None = None, + ) -> list[Message]: + # If total tokens is reached, we truncate down + # TODO: This can be broken by bad configs, e.g. lower bound too high, initial messages too fat, etc. + # TODO: `force` and `clear` seem to no longer be used, we should remove + if force or (total_tokens and total_tokens > llm_config.context_window): + self.logger.warning( + f"Total tokens {total_tokens} exceeds configured max tokens {llm_config.context_window}, forcefully clearing message history." + ) + new_in_context_messages, _updated = await self.summarizer.summarize( + in_context_messages=in_context_messages, + new_letta_messages=new_letta_messages, + force=True, + clear=True, + run_id=run_id, + step_id=step_id, + ) + else: + # NOTE (Sarah): Seems like this is doing nothing? + self.logger.info( + f"Total tokens {total_tokens} does not exceed configured max tokens {llm_config.context_window}, passing summarizing w/o force." + ) + new_in_context_messages, _updated = await self.summarizer.summarize( + in_context_messages=in_context_messages, + new_letta_messages=new_letta_messages, + run_id=run_id, + step_id=step_id, + ) + await self.agent_manager.update_message_ids_async( + agent_id=self.agent_id, + message_ids=[m.id for m in new_in_context_messages], + actor=self.actor, + ) + + return new_in_context_messages + + @trace_method + async def summarize_conversation_history(self) -> None: + """Called when the developer explicitly triggers compaction via the API""" + agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=self.agent_id, actor=self.actor) + message_ids = agent_state.message_ids + in_context_messages = await self.message_manager.get_messages_by_ids_async(message_ids=message_ids, actor=self.actor) + new_in_context_messages, _updated = await self.summarizer.summarize( + in_context_messages=in_context_messages, new_letta_messages=[], force=True + ) + return await self.agent_manager.update_message_ids_async( + agent_id=self.agent_id, message_ids=[m.id for m in new_in_context_messages], actor=self.actor + ) + + @trace_method + async def _create_llm_request_data_async( + self, + llm_client: LLMClientBase, + in_context_messages: list[Message], + agent_state: AgentState, + tool_rules_solver: ToolRulesSolver, + ) -> tuple[dict, list[str]]: + if not self.num_messages: + self.num_messages = await self.message_manager.size_async( + agent_id=agent_state.id, + actor=self.actor, + ) + if not self.num_archival_memories: + self.num_archival_memories = await self.passage_manager.agent_passage_size_async( + agent_id=agent_state.id, + actor=self.actor, + ) + + in_context_messages = await self._rebuild_memory_async( + in_context_messages, + agent_state, + num_messages=self.num_messages, + num_archival_memories=self.num_archival_memories, + tool_rules_solver=tool_rules_solver, + ) + + # scrub inner thoughts from messages if reasoning is completely disabled + in_context_messages = scrub_inner_thoughts_from_messages(in_context_messages, agent_state.llm_config) + + tools = [ + t + for t in agent_state.tools + if t.tool_type + in { + ToolType.CUSTOM, + ToolType.LETTA_CORE, + ToolType.LETTA_MEMORY_CORE, + ToolType.LETTA_MULTI_AGENT_CORE, + ToolType.LETTA_SLEEPTIME_CORE, + ToolType.LETTA_VOICE_SLEEPTIME_CORE, + ToolType.LETTA_BUILTIN, + ToolType.LETTA_FILES_CORE, + ToolType.EXTERNAL_MCP, + } + ] + + # Mirror the sync agent loop: get allowed tools or allow all if none are allowed + self.last_function_response = self._load_last_function_response(in_context_messages) + valid_tool_names = tool_rules_solver.get_allowed_tool_names( + available_tools=set([t.name for t in tools]), + last_function_response=self.last_function_response, + ) or list(set(t.name for t in tools)) + + # TODO: Copied from legacy agent loop, so please be cautious + # Set force tool + force_tool_call = None + if len(valid_tool_names) == 1: + force_tool_call = valid_tool_names[0] + + allowed_tools = [ + enable_strict_mode(t.json_schema, strict=agent_state.llm_config.strict) for t in tools if t.name in set(valid_tool_names) + ] + # Extract terminal tool names from tool rules + terminal_tool_names = {rule.tool_name for rule in tool_rules_solver.terminal_tool_rules} + allowed_tools = runtime_override_tool_json_schema( + tool_list=allowed_tools, response_format=agent_state.response_format, request_heartbeat=True, terminal_tools=terminal_tool_names + ) + + return ( + llm_client.build_request_data( + agent_state.agent_type, + in_context_messages, + agent_state.llm_config, + allowed_tools, + force_tool_call, + ), + valid_tool_names, + ) + + @trace_method + async def _handle_ai_response( + self, + tool_call: ToolCall, + valid_tool_names: list[str], + agent_state: AgentState, + tool_rules_solver: ToolRulesSolver, + usage: UsageStatistics, + reasoning_content: list[TextContent | ReasoningContent | RedactedReasoningContent | OmittedReasoningContent] | None = None, + pre_computed_assistant_message_id: str | None = None, + step_id: str | None = None, + initial_messages: list[Message] | None = None, + agent_step_span: Optional["Span"] = None, + is_final_step: bool | None = None, + run_id: str | None = None, + step_metrics: StepMetrics = None, + is_approval: bool | None = None, + is_denial: bool | None = None, + denial_reason: str | None = None, + ) -> tuple[list[Message], bool, LettaStopReason | None]: + """ + Handle the final AI response once streaming completes, execute / validate the + tool call, decide whether we should keep stepping, and persist state. + """ + tool_call_id: str = tool_call.id or f"call_{uuid.uuid4().hex[:8]}" + + if is_denial: + continue_stepping = True + stop_reason = None + tool_call_messages = create_letta_messages_from_llm_response( + agent_id=agent_state.id, + model=agent_state.llm_config.model, + function_name="", + function_arguments={}, + tool_execution_result=ToolExecutionResult(status="error"), + tool_call_id=tool_call_id, + function_response=f"Error: request to call tool denied. User reason: {denial_reason}", + timezone=agent_state.timezone, + continue_stepping=continue_stepping, + heartbeat_reason=f"{NON_USER_MSG_PREFIX}Continuing: user denied request to call tool.", + reasoning_content=None, + pre_computed_assistant_message_id=None, + step_id=step_id, + run_id=self.current_run_id, + is_approval_response=True, + ) + messages_to_persist = (initial_messages or []) + tool_call_messages + persisted_messages = await self.message_manager.create_many_messages_async( + messages_to_persist, actor=self.actor, project_id=agent_state.project_id, template_id=agent_state.template_id + ) + return persisted_messages, continue_stepping, stop_reason + + # 1. Parse and validate the tool-call envelope + tool_call_name: str = tool_call.function.name + + tool_args = _safe_load_tool_call_str(tool_call.function.arguments) + request_heartbeat: bool = _pop_heartbeat(tool_args) + tool_args.pop(INNER_THOUGHTS_KWARG, None) + + log_telemetry( + self.logger, + "_handle_ai_response execute tool start", + tool_name=tool_call_name, + tool_args=tool_args, + tool_call_id=tool_call_id, + request_heartbeat=request_heartbeat, + ) + if not is_approval and tool_rules_solver.is_requires_approval_tool(tool_call_name): + tool_args[REQUEST_HEARTBEAT_PARAM] = request_heartbeat + approval_messages = create_approval_request_message_from_llm_response( + agent_id=agent_state.id, + model=agent_state.llm_config.model, + requested_tool_calls=[ + ToolCall(id=tool_call_id, function=FunctionCall(name=tool_call_name, arguments=json.dumps(tool_args))) + ], + reasoning_content=reasoning_content, + pre_computed_assistant_message_id=pre_computed_assistant_message_id, + step_id=step_id, + ) + messages_to_persist = (initial_messages or []) + approval_messages + continue_stepping = False + stop_reason = LettaStopReason(stop_reason=StopReasonType.requires_approval.value) + else: + # 2. Execute the tool (or synthesize an error result if disallowed) + tool_rule_violated = tool_call_name not in valid_tool_names and not is_approval + if tool_rule_violated: + tool_execution_result = _build_rule_violation_result(tool_call_name, valid_tool_names, tool_rules_solver) + else: + # Track tool execution time + tool_start_time = get_utc_timestamp_ns() + tool_execution_result = await self._execute_tool( + tool_name=tool_call_name, + tool_args=tool_args, + agent_state=agent_state, + agent_step_span=agent_step_span, + step_id=step_id, + ) + tool_end_time = get_utc_timestamp_ns() + + # Store tool execution time in metrics + step_metrics.tool_execution_ns = tool_end_time - tool_start_time + + log_telemetry( + self.logger, + "_handle_ai_response execute tool finish", + tool_execution_result=tool_execution_result, + tool_call_id=tool_call_id, + ) + + # 3. Prepare the function-response payload + truncate = tool_call_name not in {"conversation_search", "conversation_search_date", "archival_memory_search"} + return_char_limit = next( + (t.return_char_limit for t in agent_state.tools if t.name == tool_call_name), + None, + ) + function_response_string = validate_function_response( + tool_execution_result.func_return, + return_char_limit=return_char_limit, + truncate=truncate, + ) + self.last_function_response = package_function_response( + was_success=tool_execution_result.success_flag, + response_string=function_response_string, + timezone=agent_state.timezone, + ) + + # 4. Decide whether to keep stepping (focal section simplified) + continue_stepping, heartbeat_reason, stop_reason = self._decide_continuation( + agent_state=agent_state, + request_heartbeat=request_heartbeat, + tool_call_name=tool_call_name, + tool_rule_violated=tool_rule_violated, + tool_rules_solver=tool_rules_solver, + is_final_step=is_final_step, + ) + + # 5. Create messages (step was already created at the beginning) + tool_call_messages = create_letta_messages_from_llm_response( + agent_id=agent_state.id, + model=agent_state.llm_config.model, + function_name=tool_call_name, + function_arguments=tool_args, + tool_execution_result=tool_execution_result, + tool_call_id=tool_call_id, + function_response=function_response_string, + timezone=agent_state.timezone, + continue_stepping=continue_stepping, + heartbeat_reason=heartbeat_reason, + reasoning_content=reasoning_content, + pre_computed_assistant_message_id=pre_computed_assistant_message_id, + step_id=step_id, + run_id=self.current_run_id, + is_approval_response=is_approval or is_denial, + ) + messages_to_persist = (initial_messages or []) + tool_call_messages + + persisted_messages = await self.message_manager.create_many_messages_async( + messages_to_persist, actor=self.actor, project_id=agent_state.project_id, template_id=agent_state.template_id + ) + + return persisted_messages, continue_stepping, stop_reason + + def _decide_continuation( + self, + agent_state: AgentState, + request_heartbeat: bool, + tool_call_name: str, + tool_rule_violated: bool, + tool_rules_solver: ToolRulesSolver, + is_final_step: bool | None, + ) -> tuple[bool, str | None, LettaStopReason | None]: + continue_stepping = request_heartbeat + heartbeat_reason: str | None = None + stop_reason: LettaStopReason | None = None + + if tool_rule_violated: + continue_stepping = True + heartbeat_reason = f"{NON_USER_MSG_PREFIX}Continuing: tool rule violation." + else: + tool_rules_solver.register_tool_call(tool_call_name) + + if tool_rules_solver.is_terminal_tool(tool_call_name): + if continue_stepping: + stop_reason = LettaStopReason(stop_reason=StopReasonType.tool_rule.value) + continue_stepping = False + + elif tool_rules_solver.has_children_tools(tool_call_name): + continue_stepping = True + heartbeat_reason = f"{NON_USER_MSG_PREFIX}Continuing: child tool rule." + + elif tool_rules_solver.is_continue_tool(tool_call_name): + continue_stepping = True + heartbeat_reason = f"{NON_USER_MSG_PREFIX}Continuing: continue tool rule." + + # – hard stop overrides – + if is_final_step: + continue_stepping = False + stop_reason = LettaStopReason(stop_reason=StopReasonType.max_steps.value) + else: + uncalled = tool_rules_solver.get_uncalled_required_tools(available_tools=set([t.name for t in agent_state.tools])) + if not continue_stepping and uncalled: + continue_stepping = True + heartbeat_reason = f"{NON_USER_MSG_PREFIX}Continuing, user expects these tools: [{', '.join(uncalled)}] to be called still." + + stop_reason = None # reset – we’re still going + + return continue_stepping, heartbeat_reason, stop_reason + + @trace_method + async def _execute_tool( + self, + tool_name: str, + tool_args: JsonDict, + agent_state: AgentState, + agent_step_span: Optional["Span"] = None, + step_id: str | None = None, + ) -> "ToolExecutionResult": + """ + Executes a tool and returns the ToolExecutionResult. + """ + from letta.schemas.tool_execution_result import ToolExecutionResult + + # Special memory case + target_tool = next((x for x in agent_state.tools if x.name == tool_name), None) + if not target_tool: + # TODO: fix this error message + return ToolExecutionResult( + func_return=f"Tool {tool_name} not found", + status="error", + ) + + # TODO: This temp. Move this logic and code to executors + + if agent_step_span: + start_time = get_utc_timestamp_ns() + agent_step_span.add_event(name="tool_execution_started") + + # Use pre-decrypted environment variable values (populated in from_orm_async) + sandbox_env_vars = {var.key: var.value or "" for var in agent_state.secrets} + tool_execution_manager = ToolExecutionManager( + agent_state=agent_state, + message_manager=self.message_manager, + agent_manager=self.agent_manager, + block_manager=self.block_manager, + job_manager=self.job_manager, + passage_manager=self.passage_manager, + sandbox_env_vars=sandbox_env_vars, + actor=self.actor, + ) + # TODO: Integrate sandbox result + log_event(name=f"start_{tool_name}_execution", attributes=tool_args) + tool_execution_result = await tool_execution_manager.execute_tool_async( + function_name=tool_name, + function_args=tool_args, + tool=target_tool, + step_id=step_id, + ) + if agent_step_span: + end_time = get_utc_timestamp_ns() + agent_step_span.add_event( + name="tool_execution_completed", + attributes={ + "tool_name": target_tool.name, + "duration_ms": ns_to_ms(end_time - start_time), + "success": tool_execution_result.success_flag, + "tool_type": target_tool.tool_type, + "tool_id": target_tool.id, + }, + ) + log_event(name=f"finish_{tool_name}_execution", attributes=tool_execution_result.model_dump()) + return tool_execution_result diff --git a/letta/agents/letta_agent_batch.py b/letta/agents/letta_agent_batch.py new file mode 100644 index 0000000..c8cebc5 --- /dev/null +++ b/letta/agents/letta_agent_batch.py @@ -0,0 +1,630 @@ +import json +import uuid +from dataclasses import dataclass +from typing import Any, AsyncGenerator, Dict, List, Optional, Sequence, Tuple, Union + +from aiomultiprocess import Pool +from anthropic.types.beta.messages import BetaMessageBatchCanceledResult, BetaMessageBatchErroredResult, BetaMessageBatchSucceededResult + +from letta.agents.base_agent import BaseAgent +from letta.agents.helpers import _prepare_in_context_messages_async +from letta.constants import DEFAULT_MAX_STEPS +from letta.helpers import ToolRulesSolver +from letta.helpers.datetime_helpers import get_utc_time +from letta.helpers.tool_execution_helper import enable_strict_mode +from letta.jobs.types import RequestStatusUpdateInfo, StepStatusUpdateInfo +from letta.llm_api.llm_client import LLMClient +from letta.local_llm.constants import INNER_THOUGHTS_KWARG +from letta.log import get_logger +from letta.otel.tracing import log_event, trace_method +from letta.schemas.agent import AgentState +from letta.schemas.enums import AgentStepStatus, JobStatus, MessageStreamStatus, ProviderType, SandboxType, ToolType +from letta.schemas.job import JobUpdate +from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage +from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, RedactedReasoningContent, TextContent +from letta.schemas.letta_request import LettaBatchRequest +from letta.schemas.letta_response import LettaBatchResponse, LettaResponse +from letta.schemas.llm_batch_job import AgentStepState, LLMBatchItem +from letta.schemas.message import Message, MessageCreate +from letta.schemas.openai.chat_completion_response import ToolCall as OpenAIToolCall +from letta.schemas.sandbox_config import SandboxConfig +from letta.schemas.tool_execution_result import ToolExecutionResult +from letta.schemas.user import User +from letta.server.rest_api.utils import create_heartbeat_system_message, create_letta_messages_from_llm_response +from letta.services.agent_manager import AgentManager +from letta.services.block_manager import BlockManager +from letta.services.job_manager import JobManager +from letta.services.llm_batch_manager import LLMBatchManager +from letta.services.message_manager import MessageManager +from letta.services.passage_manager import PassageManager +from letta.services.sandbox_config_manager import SandboxConfigManager +from letta.services.tool_executor.tool_execution_manager import ToolExecutionManager +from letta.settings import tool_settings + +logger = get_logger(__name__) + + +@dataclass +class ToolExecutionParams: + agent_id: str + tool_call_name: str + tool_args: Dict[str, Any] + agent_state: AgentState + actor: User + sbx_config: SandboxConfig + sbx_env_vars: Dict[str, Any] + + +@dataclass +class _ResumeContext: + batch_items: List[LLMBatchItem] + agent_ids: List[str] + agent_state_map: Dict[str, AgentState] + provider_results: Dict[str, Any] + tool_call_name_map: Dict[str, str] + tool_call_args_map: Dict[str, Dict[str, Any]] + should_continue_map: Dict[str, bool] + request_status_updates: List[RequestStatusUpdateInfo] + + +async def execute_tool_wrapper(params: ToolExecutionParams) -> tuple[str, ToolExecutionResult]: + """ + Executes the tool in an out‑of‑process worker and returns: + (agent_id, (tool_result:str, success_flag:bool)) + """ + from letta.schemas.tool_execution_result import ToolExecutionResult + + # locate the tool on the agent + target_tool = next((t for t in params.agent_state.tools if t.name == params.tool_call_name), None) + if not target_tool: + return params.agent_id, ToolExecutionResult(func_return=f"Tool not found: {params.tool_call_name}", status="error") + + try: + mgr = ToolExecutionManager( + agent_state=params.agent_state, + actor=params.actor, + sandbox_config=params.sbx_config, + sandbox_env_vars=params.sbx_env_vars, + ) + tool_execution_result = await mgr.execute_tool_async( + function_name=params.tool_call_name, + function_args=params.tool_args, + tool=target_tool, + ) + return params.agent_id, tool_execution_result + except Exception as e: + return params.agent_id, ToolExecutionResult(func_return=f"Failed to call tool. Error: {e}", status="error") + + +# TODO: Limitations -> +# TODO: Only works with anthropic for now +class LettaAgentBatch(BaseAgent): + def __init__( + self, + message_manager: MessageManager, + agent_manager: AgentManager, + block_manager: BlockManager, + passage_manager: PassageManager, + batch_manager: LLMBatchManager, + sandbox_config_manager: SandboxConfigManager, + job_manager: JobManager, + actor: User, + max_steps: int = DEFAULT_MAX_STEPS, + ): + self.message_manager = message_manager + self.agent_manager = agent_manager + self.block_manager = block_manager + self.passage_manager = passage_manager + self.batch_manager = batch_manager + self.sandbox_config_manager = sandbox_config_manager + self.job_manager = job_manager + self.actor = actor + self.max_steps = max_steps + self.client_skills: list = [] + + @trace_method + async def step_until_request( + self, + batch_requests: List[LettaBatchRequest], + letta_batch_job_id: str, + agent_step_state_mapping: Optional[Dict[str, AgentStepState]] = None, + ) -> LettaBatchResponse: + """Carry out agent steps until the LLM request is sent.""" + log_event(name="validate_inputs") + if not batch_requests: + raise ValueError("Empty list of batch_requests passed in!") + if agent_step_state_mapping is None: + agent_step_state_mapping = {} + + log_event(name="load_and_prepare_agents") + # prepares (1) agent states, (2) step states, (3) LLMBatchItems (4) message batch_item_ids (5) messages per agent (6) tools per agent + + agent_messages_mapping: dict[str, list[Message]] = {} + agent_tools_mapping: dict[str, list[dict]] = {} + # TODO: This isn't optimal, moving fast - prone to bugs because we pass around this half formed pydantic object + agent_batch_item_mapping: dict[str, LLMBatchItem] = {} + + # fetch agent states in batch + agent_mapping = { + agent_state.id: agent_state + for agent_state in await self.agent_manager.get_agents_by_ids_async( + agent_ids=[request.agent_id for request in batch_requests], include_relationships=["tools", "memory"], actor=self.actor + ) + } + + agent_states = [] + for batch_request in batch_requests: + agent_id = batch_request.agent_id + agent_state = agent_mapping[agent_id] + agent_states.append(agent_state) # keeping this to maintain ordering, but may not be necessary + + if agent_id not in agent_step_state_mapping: + agent_step_state_mapping[agent_id] = AgentStepState( + step_number=0, tool_rules_solver=ToolRulesSolver(tool_rules=agent_state.tool_rules) + ) + + llm_batch_item = LLMBatchItem( + llm_batch_id="", # TODO: This is hacky, it gets filled in later + agent_id=agent_state.id, + llm_config=agent_state.llm_config, + request_status=JobStatus.created, + step_status=AgentStepStatus.paused, + step_state=agent_step_state_mapping[agent_id], + ) + agent_batch_item_mapping[agent_id] = llm_batch_item + + # Fill in the batch_item_id for the message + for msg in batch_request.messages: + msg.batch_item_id = llm_batch_item.id + + agent_messages_mapping[agent_id] = await self._prepare_in_context_messages_per_agent_async( + agent_state=agent_state, input_messages=batch_request.messages + ) + + agent_tools_mapping[agent_id] = self._prepare_tools_per_agent(agent_state, agent_step_state_mapping[agent_id].tool_rules_solver) + + log_event(name="init_llm_client") + llm_client = LLMClient.create( + provider_type=agent_states[0].llm_config.model_endpoint_type, + put_inner_thoughts_first=True, + actor=self.actor, + ) + agent_llm_config_mapping = {s.id: s.llm_config for s in agent_states} + + log_event(name="send_llm_batch_request") + batch_response = await llm_client.send_llm_batch_request_async( + agent_type=agent_states[0].agent_type, + agent_messages_mapping=agent_messages_mapping, + agent_tools_mapping=agent_tools_mapping, + agent_llm_config_mapping=agent_llm_config_mapping, + ) + + log_event(name="persist_llm_batch_job") + llm_batch_job = await self.batch_manager.create_llm_batch_job_async( + llm_provider=ProviderType.anthropic, # TODO: Expand to more providers + create_batch_response=batch_response, + actor=self.actor, + status=JobStatus.running, + letta_batch_job_id=letta_batch_job_id, + ) + + log_event(name="prepare_batch_items") + batch_items = [] + for state in agent_states: + llm_batch_item = agent_batch_item_mapping[state.id] + # TODO This is hacky + llm_batch_item.llm_batch_id = llm_batch_job.id + batch_items.append(llm_batch_item) + + if batch_items: + log_event(name="bulk_create_batch_items") + await self.batch_manager.create_llm_batch_items_bulk_async(batch_items, actor=self.actor) + + log_event(name="return_batch_response") + return LettaBatchResponse( + letta_batch_id=llm_batch_job.letta_batch_job_id, + last_llm_batch_id=llm_batch_job.id, + status=llm_batch_job.status, + agent_count=len(agent_states), + last_polled_at=get_utc_time(), + created_at=llm_batch_job.created_at, + ) + + @trace_method + async def resume_step_after_request(self, letta_batch_id: str, llm_batch_id: str) -> LettaBatchResponse: + log_event(name="load_context") + llm_batch_job = await self.batch_manager.get_llm_batch_job_by_id_async(llm_batch_id=llm_batch_id, actor=self.actor) + ctx = await self._collect_resume_context(llm_batch_id) + + log_event(name="update_statuses") + await self._update_request_statuses_async(ctx.request_status_updates) + + log_event(name="exec_tools") + exec_results = await self._execute_tools(ctx) + + log_event(name="persist_messages") + msg_map = await self._persist_tool_messages(exec_results, ctx) + + log_event(name="mark_steps_done") + await self._mark_steps_complete_async(llm_batch_id, ctx.agent_ids) + + log_event(name="prepare_next") + next_reqs, next_step_state = await self._prepare_next_iteration_async(exec_results, ctx, msg_map) + if len(next_reqs) == 0: + await self.job_manager.update_job_by_id_async( + job_id=letta_batch_id, job_update=JobUpdate(status=JobStatus.completed), actor=self.actor + ) + return LettaBatchResponse( + letta_batch_id=llm_batch_job.letta_batch_job_id, + last_llm_batch_id=llm_batch_job.id, + status=JobStatus.completed, + agent_count=len(ctx.agent_ids), + last_polled_at=get_utc_time(), + created_at=llm_batch_job.created_at, + ) + + return await self.step_until_request( + batch_requests=next_reqs, + letta_batch_job_id=letta_batch_id, + agent_step_state_mapping=next_step_state, + ) + + @trace_method + async def _collect_resume_context(self, llm_batch_id: str) -> _ResumeContext: + """ + Collect context for resuming operations from completed batch items. + + Args: + llm_batch_id: The ID of the batch to collect context for + + Returns: + _ResumeContext object containing all necessary data for resumption + """ + # Fetch only completed batch items + batch_items = await self.batch_manager.list_llm_batch_items_async(llm_batch_id=llm_batch_id, request_status=JobStatus.completed) + + # Exit early if no items to process + if not batch_items: + return _ResumeContext( + batch_items=[], + agent_ids=[], + agent_state_map={}, + provider_results={}, + tool_call_name_map={}, + tool_call_args_map={}, + should_continue_map={}, + request_status_updates=[], + ) + + # Extract agent IDs and organize items by agent ID + agent_ids = [item.agent_id for item in batch_items] + batch_item_map = {item.agent_id: item for item in batch_items} + + # Collect provider results + provider_results = {item.agent_id: item.batch_request_result.result for item in batch_items} + + # Fetch agent states in a single call + agent_states = await self.agent_manager.get_agents_by_ids_async( + agent_ids=agent_ids, include_relationships=["tools", "memory"], actor=self.actor + ) + agent_state_map = {agent.id: agent for agent in agent_states} + + # Process each agent's results + tool_call_results = await self._process_agent_results( + agent_ids=agent_ids, batch_item_map=batch_item_map, provider_results=provider_results, llm_batch_id=llm_batch_id + ) + + return _ResumeContext( + batch_items=batch_items, + agent_ids=agent_ids, + agent_state_map=agent_state_map, + provider_results=provider_results, + tool_call_name_map=tool_call_results.name_map, + tool_call_args_map=tool_call_results.args_map, + should_continue_map=tool_call_results.cont_map, + request_status_updates=tool_call_results.status_updates, + ) + + async def _process_agent_results(self, agent_ids, batch_item_map, provider_results, llm_batch_id): + """ + Process the results for each agent, extracting tool calls and determining continuation status. + + Returns: + A namedtuple containing name_map, args_map, cont_map, and status_updates + """ + from collections import namedtuple + + ToolCallResults = namedtuple("ToolCallResults", ["name_map", "args_map", "cont_map", "status_updates"]) + + name_map, args_map, cont_map = {}, {}, {} + request_status_updates = [] + + for aid in agent_ids: + item = batch_item_map[aid] + result = provider_results[aid] + + # Determine job status based on result type + status = self._determine_job_status(result) + request_status_updates.append(RequestStatusUpdateInfo(llm_batch_id=llm_batch_id, agent_id=aid, request_status=status)) + + # Process tool calls + name, args, cont = await self._extract_tool_call_from_result(item, result) + name_map[aid], args_map[aid], cont_map[aid] = name, args, cont + + return ToolCallResults(name_map, args_map, cont_map, request_status_updates) + + def _determine_job_status(self, result): + """Determine job status based on result type""" + if isinstance(result, BetaMessageBatchSucceededResult): + return JobStatus.completed + elif isinstance(result, BetaMessageBatchErroredResult): + return JobStatus.failed + elif isinstance(result, BetaMessageBatchCanceledResult): + return JobStatus.cancelled + else: + return JobStatus.expired + + async def _extract_tool_call_from_result(self, item, result): + """Extract tool call information from a result""" + llm_client = LLMClient.create( + provider_type=item.llm_config.model_endpoint_type, + put_inner_thoughts_first=True, + actor=self.actor, + ) + + # If result isn't a successful type, we can't extract a tool call + if not isinstance(result, BetaMessageBatchSucceededResult): + return None, None, False + + response = await llm_client.convert_response_to_chat_completion( + response_data=result.message.model_dump(), input_messages=[], llm_config=item.llm_config + ) + tool_call = response.choices[0].message.tool_calls[0] + + return self._extract_tool_call_and_decide_continue(tool_call, item.step_state) + + async def _update_request_statuses_async(self, updates: List[RequestStatusUpdateInfo]) -> None: + if updates: + await self.batch_manager.bulk_update_llm_batch_items_request_status_by_agent_async(updates=updates) + + async def _build_sandbox(self) -> Tuple[SandboxConfig, Dict[str, Any]]: + sbx_type = SandboxType.E2B if tool_settings.e2b_api_key else SandboxType.LOCAL + cfg = await self.sandbox_config_manager.get_or_create_default_sandbox_config_async(sandbox_type=sbx_type, actor=self.actor) + env = await self.sandbox_config_manager.get_sandbox_env_vars_as_dict_async(cfg.id, actor=self.actor, limit=100) + return cfg, env + + @trace_method + async def _execute_tools(self, ctx: _ResumeContext) -> Sequence[tuple[str, ToolExecutionResult]]: + sbx_cfg, sbx_env = await self._build_sandbox() + rethink_memory_tool_name = "rethink_memory" + tool_params = [] + # TODO: This is a special case - we need to think about how to generalize this + # TODO: Rethink memory is a common op that is easily batchable, so we pull this logic out + rethink_memory_params = [] + for aid in ctx.agent_ids: + param = ToolExecutionParams( + agent_id=aid, + tool_call_name=ctx.tool_call_name_map[aid], + tool_args=ctx.tool_call_args_map[aid], + agent_state=ctx.agent_state_map[aid], + actor=self.actor, + sbx_config=sbx_cfg, + sbx_env_vars=sbx_env, + ) + + if ctx.tool_call_name_map[aid] == rethink_memory_tool_name: + rethink_memory_params.append(param) + else: + tool_params.append(param) + + if rethink_memory_params: + return await self._bulk_rethink_memory_async(rethink_memory_params) + + if tool_params: + async with Pool() as pool: + return await pool.map(execute_tool_wrapper, tool_params) + + @trace_method + async def _bulk_rethink_memory_async(self, params: List[ToolExecutionParams]) -> Sequence[tuple[str, ToolExecutionResult]]: + updates = {} + result = [] + for param in params: + # Sanity check + # TODO: This is very brittle and done quickly for performance + # TODO: If the end tool is changed, this will break + # TODO: Move 'rethink_memory' to a native Letta tool that we control + if "new_memory" not in param.tool_args or "target_block_label" not in param.tool_args: + raise ValueError(f"Missing either `new_memory` or `target_block_label` in the tool args: {param.tool_args}") + + # Find the block id/update + block_id = param.agent_state.memory.get_block(label=param.tool_args.get("target_block_label")).id + new_value = param.tool_args.get("new_memory") + + # This is sensitive to multiple agents overwriting the same memory block + updates[block_id] = new_value + + # TODO: This is quite ugly and confusing - this is mostly to align with the returns of other tools + result.append((param.agent_id, ToolExecutionResult(status="success"))) + + await self.block_manager.bulk_update_block_values_async(updates=updates, actor=self.actor) + + return result + + async def _persist_tool_messages( + self, + exec_results: Sequence[Tuple[str, "ToolExecutionResult"]], + ctx: _ResumeContext, + ) -> Dict[str, List[Message]]: + # TODO: This is redundant, we should have this ready on the ctx + # TODO: I am doing it quick and dirty for now + agent_item_map: Dict[str, LLMBatchItem] = {item.agent_id: item for item in ctx.batch_items} + + msg_map: Dict[str, List[Message]] = {} + for aid, tool_exec_result in exec_results: + msgs = self._create_tool_call_messages( + llm_batch_item_id=agent_item_map[aid].id, + agent_state=ctx.agent_state_map[aid], + tool_call_name=ctx.tool_call_name_map[aid], + tool_call_args=ctx.tool_call_args_map[aid], + tool_exec_result=tool_exec_result.func_return, + success_flag=tool_exec_result.success_flag, + tool_exec_result_obj=tool_exec_result, + reasoning_content=None, + ) + msg_map[aid] = msgs + # flatten & persist + await self.message_manager.create_many_messages_async([m for msgs in msg_map.values() for m in msgs], actor=self.actor) + return msg_map + + async def _mark_steps_complete_async(self, llm_batch_id: str, agent_ids: List[str]) -> None: + updates = [ + StepStatusUpdateInfo(llm_batch_id=llm_batch_id, agent_id=aid, step_status=AgentStepStatus.completed) for aid in agent_ids + ] + await self.batch_manager.bulk_update_llm_batch_items_step_status_by_agent_async(updates) + + async def _prepare_next_iteration_async( + self, + exec_results: Sequence[Tuple[str, "ToolExecutionResult"]], + ctx: _ResumeContext, + msg_map: Dict[str, List[Message]], + ) -> Tuple[List[LettaBatchRequest], Dict[str, AgentStepState]]: + # who continues? + continues = [agent_id for agent_id, cont in ctx.should_continue_map.items() if cont] + + success_flag_map = {aid: result.success_flag for aid, result in exec_results} + + batch_reqs: List[LettaBatchRequest] = [] + for agent_id in continues: + heartbeat = create_heartbeat_system_message( + agent_id=agent_id, + model=ctx.agent_state_map[agent_id].llm_config.model, + function_call_success=success_flag_map[agent_id], + timezone=ctx.agent_state_map[agent_id].timezone, + ) + batch_reqs.append( + LettaBatchRequest( + agent_id=agent_id, + messages=[MessageCreate.model_validate(heartbeat.model_dump(include={"role", "content", "name", "otid"}))], + ) + ) + + # extend in‑context ids when necessary + for agent_id, new_msgs in msg_map.items(): + ast = ctx.agent_state_map[agent_id] + if not ast.message_buffer_autoclear: + await self.agent_manager.update_message_ids_async( + agent_id=agent_id, + message_ids=ast.message_ids + [m.id for m in new_msgs], + actor=self.actor, + ) + + # bump step number + step_map = { + item.agent_id: item.step_state.model_copy(update={"step_number": item.step_state.step_number + 1}) for item in ctx.batch_items + } + return batch_reqs, step_map + + def _create_tool_call_messages( + self, + llm_batch_item_id: str, + agent_state: AgentState, + tool_call_name: str, + tool_call_args: Dict[str, Any], + tool_exec_result: str, + tool_exec_result_obj: "ToolExecutionResult", + success_flag: bool, + reasoning_content: Optional[List[Union[TextContent, ReasoningContent, RedactedReasoningContent, OmittedReasoningContent]]] = None, + ) -> List[Message]: + tool_call_id = f"call_{uuid.uuid4().hex[:8]}" + + tool_call_messages = create_letta_messages_from_llm_response( + agent_id=agent_state.id, + model=agent_state.llm_config.model, + function_name=tool_call_name, + function_arguments=tool_call_args, + tool_call_id=tool_call_id, + function_response=tool_exec_result, + tool_execution_result=tool_exec_result_obj, + timezone=agent_state.timezone, + continue_stepping=False, + reasoning_content=reasoning_content, + pre_computed_assistant_message_id=None, + llm_batch_item_id=llm_batch_item_id, + ) + + return tool_call_messages + + # TODO: This is doing a lot of dict passing + # TODO: Make the passing here typed + def _extract_tool_call_and_decide_continue( + self, tool_call: OpenAIToolCall, agent_step_state: AgentStepState + ) -> Tuple[str, Dict[str, Any], bool]: + """ + Now that streaming is done, handle the final AI response. + This might yield additional SSE tokens if we do stalling. + At the end, set self._continue_execution accordingly. + """ + tool_call_name = tool_call.function.name + tool_call_args_str = tool_call.function.arguments + + try: + tool_args = json.loads(tool_call_args_str) + except json.JSONDecodeError: + logger.warning(f"Failed to JSON decode tool call argument string: {tool_call_args_str}") + tool_args = {} + + # Get request heartbeats and coerce to bool + request_heartbeat = tool_args.pop("request_heartbeat", False) + # Pre-emptively pop out inner_thoughts + tool_args.pop(INNER_THOUGHTS_KWARG, "") + + # So this is necessary, because sometimes non-structured outputs makes mistakes + if isinstance(request_heartbeat, str): + request_heartbeat = request_heartbeat.lower() == "true" + else: + request_heartbeat = bool(request_heartbeat) + + continue_stepping = request_heartbeat + tool_rules_solver = agent_step_state.tool_rules_solver + tool_rules_solver.register_tool_call(tool_name=tool_call_name) + if tool_rules_solver.is_terminal_tool(tool_name=tool_call_name): + continue_stepping = False + elif tool_rules_solver.has_children_tools(tool_name=tool_call_name): + continue_stepping = True + elif tool_rules_solver.is_continue_tool(tool_name=tool_call_name): + continue_stepping = True + + step_count = agent_step_state.step_number + if step_count >= self.max_steps: + logger.warning("Hit max steps, stopping agent loop prematurely.") + continue_stepping = False + + return tool_call_name, tool_args, continue_stepping + + @staticmethod + def _prepare_tools_per_agent(agent_state: AgentState, tool_rules_solver: ToolRulesSolver) -> List[dict]: + tools = [t for t in agent_state.tools if t.tool_type in {ToolType.CUSTOM, ToolType.LETTA_CORE, ToolType.LETTA_MEMORY_CORE}] + valid_tool_names = tool_rules_solver.get_allowed_tool_names(available_tools=set([t.name for t in tools])) + return [enable_strict_mode(t.json_schema, strict=agent_state.llm_config.strict) for t in tools if t.name in set(valid_tool_names)] + + async def _prepare_in_context_messages_per_agent_async( + self, agent_state: AgentState, input_messages: List[MessageCreate] + ) -> List[Message]: + current_in_context_messages, new_in_context_messages = await _prepare_in_context_messages_async( + input_messages, agent_state, self.message_manager, self.actor, run_id=None + ) + + self.conversation_id = None + in_context_messages = await self._rebuild_memory_async(current_in_context_messages + new_in_context_messages, agent_state) + return in_context_messages + + # Not used in batch. + async def step( + self, input_messages: List[MessageCreate], max_steps: int = DEFAULT_MAX_STEPS, run_id: str | None = None + ) -> LettaResponse: + raise NotImplementedError + + async def step_stream( + self, input_messages: List[MessageCreate], max_steps: int = DEFAULT_MAX_STEPS + ) -> AsyncGenerator[Union[LettaMessage, LegacyLettaMessage, MessageStreamStatus], None]: + raise NotImplementedError diff --git a/letta/agents/letta_agent_v2.py b/letta/agents/letta_agent_v2.py new file mode 100644 index 0000000..961b550 --- /dev/null +++ b/letta/agents/letta_agent_v2.py @@ -0,0 +1,1487 @@ +import json +import uuid +from datetime import datetime +from typing import AsyncGenerator, Optional, Tuple + +from opentelemetry.trace import Span + +from letta.adapters.letta_llm_adapter import LettaLLMAdapter +from letta.adapters.letta_llm_request_adapter import LettaLLMRequestAdapter +from letta.adapters.letta_llm_stream_adapter import LettaLLMStreamAdapter +from letta.agents.base_agent_v2 import BaseAgentV2 +from letta.agents.helpers import ( + _build_rule_violation_result, + _load_last_function_response, + _maybe_get_approval_messages, + _pop_heartbeat, + _prepare_in_context_messages_no_persist_async, + _safe_load_tool_call_str, + generate_step_id, +) +from letta.constants import DEFAULT_MAX_STEPS, NON_USER_MSG_PREFIX, REQUEST_HEARTBEAT_PARAM +from letta.errors import ContextWindowExceededError, InsufficientCreditsError, LLMError +from letta.helpers import ToolRulesSolver +from letta.helpers.datetime_helpers import get_utc_time, get_utc_timestamp_ns, ns_to_ms +from letta.helpers.reasoning_helper import scrub_inner_thoughts_from_messages +from letta.helpers.tool_execution_helper import enable_strict_mode +from letta.llm_api.llm_client import LLMClient +from letta.local_llm.constants import INNER_THOUGHTS_KWARG +from letta.log import get_logger +from letta.otel.tracing import log_event, trace_method, tracer +from letta.prompts.prompt_generator import PromptGenerator +from letta.schemas.agent import AgentState, UpdateAgent +from letta.schemas.enums import AgentType, LLMCallType, MessageStreamStatus, RunStatus, StepStatus +from letta.schemas.letta_message import LettaMessage, MessageType +from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, RedactedReasoningContent, TextContent +from letta.schemas.letta_request import ClientSkillSchema, ClientToolSchema +from letta.schemas.letta_response import LettaResponse +from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType +from letta.schemas.message import Message, MessageCreate, MessageUpdate +from letta.schemas.openai.chat_completion_response import ( + FunctionCall, + ToolCall, + UsageStatistics, + UsageStatisticsCompletionTokenDetails, + UsageStatisticsPromptTokenDetails, +) +from letta.schemas.provider_trace import BillingContext +from letta.schemas.step import Step, StepProgression +from letta.schemas.step_metrics import StepMetrics +from letta.schemas.tool import Tool +from letta.schemas.tool_execution_result import ToolExecutionResult +from letta.schemas.usage import LettaUsageStatistics +from letta.schemas.user import User +from letta.server.rest_api.utils import ( + create_approval_request_message_from_llm_response, + create_letta_messages_from_llm_response, +) +from letta.services.agent_manager import AgentManager +from letta.services.archive_manager import ArchiveManager +from letta.services.block_manager import BlockManager +from letta.services.credit_verification_service import CreditVerificationService +from letta.services.helpers.tool_parser_helper import runtime_override_tool_json_schema +from letta.services.message_manager import MessageManager +from letta.services.passage_manager import PassageManager +from letta.services.run_manager import RunManager +from letta.services.step_manager import StepManager +from letta.services.summarizer.enums import SummarizationMode +from letta.services.summarizer.summarizer import Summarizer +from letta.services.telemetry_manager import TelemetryManager +from letta.services.tool_executor.tool_execution_manager import ToolExecutionManager +from letta.settings import settings, summarizer_settings +from letta.system import package_function_response +from letta.types import JsonDict +from letta.utils import log_telemetry, safe_create_task, safe_create_task_with_return, united_diff, validate_function_response + + +class LettaAgentV2(BaseAgentV2): + """ + Abstract base class for the Letta agent loop, handling message management, + LLM API requests, tool execution, and context tracking. + + This implementation uses a unified execution path through the _step method, + supporting both blocking and streaming LLM interactions via the adapter pattern. + """ + + def __init__( + self, + agent_state: AgentState, + actor: User, + ): + super().__init__(agent_state, actor) + self.logger = get_logger(agent_state.id) + self.tool_rules_solver = ToolRulesSolver(tool_rules=agent_state.tool_rules) + self.llm_client = LLMClient.create( + provider_type=agent_state.llm_config.model_endpoint_type, + put_inner_thoughts_first=True, + actor=actor, + ) + self._initialize_state() + + # Manager classes + self.agent_manager = AgentManager() + self.archive_manager = ArchiveManager() + self.block_manager = BlockManager() + self.run_manager = RunManager() + self.message_manager = MessageManager() + self.passage_manager = PassageManager() + self.step_manager = StepManager() + self.telemetry_manager = TelemetryManager() + self.credit_verification_service = CreditVerificationService() + + ## TODO: Expand to more + # if summarizer_settings.enable_summarization and model_settings.openai_api_key: + # self.summarization_agent = EphemeralSummaryAgent( + # target_block_label="conversation_summary", + # agent_id=self.agent_state.id, + # block_manager=self.block_manager, + # message_manager=self.message_manager, + # agent_manager=self.agent_manager, + # actor=self.actor, + # ) + + # Initialize summarizer for context window management + self.summarizer = Summarizer( + mode=( + SummarizationMode.STATIC_MESSAGE_BUFFER + if self.agent_state.agent_type == AgentType.voice_convo_agent + else summarizer_settings.mode + ), + summarizer_agent=None, # self.summarization_agent, + message_buffer_limit=summarizer_settings.message_buffer_limit, + message_buffer_min=summarizer_settings.message_buffer_min, + partial_evict_summarizer_percentage=summarizer_settings.partial_evict_summarizer_percentage, + agent_manager=self.agent_manager, + message_manager=self.message_manager, + actor=self.actor, + agent_id=self.agent_state.id, + ) + + @trace_method + async def build_request( + self, + input_messages: list[MessageCreate], + client_skills: list[ClientSkillSchema] | None = None, + client_tools: list[ClientToolSchema] | None = None, + conversation_id: str | None = None, + override_system: str | None = None, + ) -> dict: + """ + Build the request data for an LLM call without actually executing it. + + This is useful for debugging and testing to see what would be sent to the LLM. + + Args: + input_messages: List of new messages to process + client_skills: Optional client-side skills to include in system prompt + client_tools: Optional client-side tools to include in tool list (V2 ignores, V3 uses) + conversation_id: Optional conversation ID (V2 ignores, V3 uses for scoped context) + + Returns: + dict: The request data that would be sent to the LLM + """ + request = {} + self.client_skills = client_skills or [] + self.override_system = override_system + in_context_messages, input_messages_to_persist = await _prepare_in_context_messages_no_persist_async( + input_messages, self.agent_state, self.message_manager, self.actor, None + ) + response = self._step( + run_id=None, + messages=in_context_messages + input_messages_to_persist, + llm_adapter=LettaLLMRequestAdapter( + llm_client=self.llm_client, + llm_config=self.agent_state.llm_config, + call_type=LLMCallType.agent_step, + agent_id=self.agent_state.id, + agent_tags=self.agent_state.tags, + org_id=self.actor.organization_id, + user_id=self.actor.id, + ), + dry_run=True, + enforce_run_id_set=False, + ) + async for chunk in response: + request = chunk # First chunk contains request data + break + + return request + + @trace_method + async def step( + self, + input_messages: list[MessageCreate], + max_steps: int = DEFAULT_MAX_STEPS, + run_id: str | None = None, + use_assistant_message: bool = True, + include_return_message_types: list[MessageType] | None = None, + request_start_timestamp_ns: int | None = None, + client_tools: list[ClientToolSchema] | None = None, + client_skills: list[ClientSkillSchema] | None = None, + override_system: str | None = None, + include_compaction_messages: bool = False, # Not used in V2, but accepted for API compatibility + billing_context: "BillingContext | None" = None, + ) -> LettaResponse: + """ + Execute the agent loop in blocking mode, returning all messages at once. + + Args: + input_messages: List of new messages to process + max_steps: Maximum number of agent steps to execute + run_id: Optional job/run ID for tracking + use_assistant_message: Whether to use assistant message format + include_return_message_types: Filter for which message types to return + request_start_timestamp_ns: Start time for tracking request duration + client_tools: Optional list of client-side tools (not used in V2, for API compatibility) + include_compaction_messages: Not used in V2, but accepted for API compatibility. + + Returns: + LettaResponse: Complete response with all messages and metadata + """ + self._initialize_state() + self.conversation_id = None + self.client_skills = client_skills or [] + self.override_system = override_system + request_span = self._request_checkpoint_start(request_start_timestamp_ns=request_start_timestamp_ns) + + in_context_messages, input_messages_to_persist = await _prepare_in_context_messages_no_persist_async( + input_messages, self.agent_state, self.message_manager, self.actor, run_id + ) + in_context_messages = in_context_messages + input_messages_to_persist + response_letta_messages = [] + credit_task = None + for i in range(max_steps): + remaining_turns = max_steps - i - 1 + + # Await credit check from previous iteration before running next step + if credit_task is not None: + if not await credit_task: + self.should_continue = False + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.insufficient_credits) + break + credit_task = None + + response = self._step( + messages=in_context_messages + self.response_messages, + input_messages_to_persist=input_messages_to_persist, + llm_adapter=LettaLLMRequestAdapter( + llm_client=self.llm_client, + llm_config=self.agent_state.llm_config, + call_type=LLMCallType.agent_step, + agent_id=self.agent_state.id, + agent_tags=self.agent_state.tags, + run_id=run_id, + org_id=self.actor.organization_id, + user_id=self.actor.id, + ), + run_id=run_id, + use_assistant_message=use_assistant_message, + include_return_message_types=include_return_message_types, + request_start_timestamp_ns=request_start_timestamp_ns, + remaining_turns=remaining_turns, + ) + + async for chunk in response: + response_letta_messages.append(chunk) + + if not self.should_continue: + break + + # Fire credit check to run in parallel with loop overhead / next step setup + credit_task = safe_create_task_with_return(self._check_credits()) + + input_messages_to_persist = [] + + # Rebuild context window after stepping + if not self.agent_state.message_buffer_autoclear: + await self.summarize_conversation_history( + in_context_messages=in_context_messages, + new_letta_messages=self.response_messages, + total_tokens=self.usage.total_tokens, + force=False, + run_id=run_id, + ) + + if self.stop_reason is None: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + + result = LettaResponse(messages=response_letta_messages, stop_reason=self.stop_reason, usage=self.usage) + if run_id: + if self.job_update_metadata is None: + self.job_update_metadata = {} + self.job_update_metadata["result"] = result.model_dump(mode="json") + + await self._request_checkpoint_finish( + request_span=request_span, request_start_timestamp_ns=request_start_timestamp_ns, run_id=run_id + ) + return result + + @trace_method + async def stream( + self, + input_messages: list[MessageCreate], + max_steps: int = DEFAULT_MAX_STEPS, + stream_tokens: bool = False, + run_id: str | None = None, + use_assistant_message: bool = True, + include_return_message_types: list[MessageType] | None = None, + request_start_timestamp_ns: int | None = None, + conversation_id: str | None = None, # Not used in V2, but accepted for API compatibility + client_tools: list[ClientToolSchema] | None = None, + client_skills: list[ClientSkillSchema] | None = None, + override_system: str | None = None, + include_compaction_messages: bool = False, # Not used in V2, but accepted for API compatibility + billing_context: BillingContext | None = None, + openai_responses_websocket: bool = False, # Not used in V2, but accepted for API compatibility + ) -> AsyncGenerator[str, None]: + """ + Execute the agent loop in streaming mode, yielding chunks as they become available. + If stream_tokens is True, individual tokens are streamed as they arrive from the LLM, + providing the lowest latency experience, otherwise each complete step (reasoning + + tool call + tool return) is yielded as it completes. + + Args: + input_messages: List of new messages to process + max_steps: Maximum number of agent steps to execute + stream_tokens: Whether to stream back individual tokens. Not all llm + providers offer native token streaming functionality; in these cases, + this api streams back steps rather than individual tokens. + run_id: Optional job/run ID for tracking + use_assistant_message: Whether to use assistant message format + include_return_message_types: Filter for which message types to return + request_start_timestamp_ns: Start time for tracking request duration + client_tools: Optional list of client-side tools (not used in V2, for API compatibility) + include_compaction_messages: Not used in V2, but accepted for API compatibility. + + Yields: + str: JSON-formatted SSE data chunks for each completed step + """ + self._initialize_state() + self.conversation_id = conversation_id + self.client_skills = client_skills or [] + self.override_system = override_system + request_span = self._request_checkpoint_start(request_start_timestamp_ns=request_start_timestamp_ns) + first_chunk = True + + if stream_tokens: + llm_adapter = LettaLLMStreamAdapter( + llm_client=self.llm_client, + llm_config=self.agent_state.llm_config, + call_type=LLMCallType.agent_step, + agent_id=self.agent_state.id, + agent_tags=self.agent_state.tags, + run_id=run_id, + org_id=self.actor.organization_id, + user_id=self.actor.id, + ) + else: + llm_adapter = LettaLLMRequestAdapter( + llm_client=self.llm_client, + llm_config=self.agent_state.llm_config, + call_type=LLMCallType.agent_step, + agent_id=self.agent_state.id, + agent_tags=self.agent_state.tags, + run_id=run_id, + org_id=self.actor.organization_id, + user_id=self.actor.id, + ) + + try: + in_context_messages, input_messages_to_persist = await _prepare_in_context_messages_no_persist_async( + input_messages, self.agent_state, self.message_manager, self.actor, run_id + ) + in_context_messages = in_context_messages + input_messages_to_persist + credit_task = None + for i in range(max_steps): + # Await credit check from previous iteration before running next step + if credit_task is not None: + if not await credit_task: + self.should_continue = False + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.insufficient_credits) + break + credit_task = None + + response = self._step( + messages=in_context_messages + self.response_messages, + input_messages_to_persist=input_messages_to_persist, + llm_adapter=llm_adapter, + run_id=run_id, + use_assistant_message=use_assistant_message, + include_return_message_types=include_return_message_types, + request_start_timestamp_ns=request_start_timestamp_ns, + ) + async for chunk in response: + if first_chunk: + request_span = self._request_checkpoint_ttft(request_span, request_start_timestamp_ns) + yield f"data: {chunk.model_dump_json()}\n\n" + first_chunk = False + + if not self.should_continue: + break + + # Fire credit check to run in parallel with loop overhead / next step setup + credit_task = safe_create_task_with_return(self._check_credits()) + + input_messages_to_persist = [] + + if self.stop_reason is None: + # terminated due to hitting max_steps + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.max_steps.value) + + if not self.agent_state.message_buffer_autoclear: + await self.summarize_conversation_history( + in_context_messages=in_context_messages, + new_letta_messages=self.response_messages, + total_tokens=self.usage.total_tokens, + force=False, + run_id=run_id, + ) + + except: + if self.stop_reason and not first_chunk: + yield f"data: {self.stop_reason.model_dump_json()}\n\n" + raise + + if run_id: + letta_messages = Message.to_letta_messages_from_list( + self.response_messages, + use_assistant_message=use_assistant_message, + reverse=False, + ) + if not self.stop_reason: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + result = LettaResponse(messages=letta_messages, stop_reason=self.stop_reason, usage=self.usage) + if self.job_update_metadata is None: + self.job_update_metadata = {} + self.job_update_metadata["result"] = result.model_dump(mode="json") + + await self._request_checkpoint_finish( + request_span=request_span, request_start_timestamp_ns=request_start_timestamp_ns, run_id=run_id + ) + for finish_chunk in self.get_finish_chunks_for_stream(self.usage, self.stop_reason): + yield f"data: {finish_chunk}\n\n" + + @trace_method + async def _step( + self, + messages: list[Message], + llm_adapter: LettaLLMAdapter, + run_id: Optional[str], + input_messages_to_persist: list[Message] | None = None, + use_assistant_message: bool = True, + include_return_message_types: list[MessageType] | None = None, + request_start_timestamp_ns: int | None = None, + remaining_turns: int = -1, + dry_run: bool = False, + enforce_run_id_set: bool = True, + ) -> AsyncGenerator[LettaMessage | dict, None]: + """ + Execute a single agent step (one LLM call and tool execution). + + This is the core execution method that all public methods (step, stream_steps, + stream_tokens) funnel through. It handles the complete flow of making an LLM + request, processing the response, executing tools, and persisting messages. + + Args: + messages: Current in-context messages + llm_adapter: Adapter for LLM interaction (blocking or streaming) + input_messages_to_persist: New messages to persist after execution + run_id: Optional job/run ID for tracking + use_assistant_message: Whether to use assistant message format + include_return_message_types: Filter for which message types to yield + request_start_timestamp_ns: Start time for tracking request duration + remaining_turns: Number of turns remaining (for max_steps enforcement) + dry_run: If true, only build and return the request without executing + + Yields: + LettaMessage or dict: Chunks for streaming mode, or request data for dry_run + """ + if enforce_run_id_set and run_id is None: + raise AssertionError("run_id is required when enforce_run_id_set is True") + + step_progression = StepProgression.START + caught_exception = None + # TODO(@caren): clean this up + tool_call, reasoning_content, agent_step_span, first_chunk, step_id, logged_step, _step_start_ns, step_metrics = ( + None, + None, + None, + None, + None, + None, + None, + None, + ) + try: + self.last_function_response = _load_last_function_response(messages) + valid_tools = await self._get_valid_tools() + approval_request, approval_response = _maybe_get_approval_messages(messages) + if approval_request and approval_response: + tool_call = approval_request.tool_calls[0] + reasoning_content = approval_request.content + step_id = approval_request.step_id + step_metrics = await self.step_manager.get_step_metrics_async(step_id=step_id, actor=self.actor) + else: + # Check for job cancellation at the start of each step + if run_id and await self._check_run_cancellation(run_id): + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.cancelled.value) + self.logger.info(f"Agent execution cancelled for run {run_id}") + return + + step_id = generate_step_id() + step_progression, logged_step, step_metrics, agent_step_span = await self._step_checkpoint_start( + step_id=step_id, run_id=run_id + ) + + messages = await self._refresh_messages(messages) + force_tool_call = valid_tools[0]["name"] if len(valid_tools) == 1 else None + for llm_request_attempt in range(summarizer_settings.max_summarizer_retries + 1): + try: + request_system_prompt = self.generate_request_system_prompt( + client_skills=self.client_skills, + current_system_message=messages[0], + ) + request_data = self.llm_client.build_request_data( + agent_type=self.agent_state.agent_type, + messages=messages, + llm_config=self.agent_state.llm_config, + tools=valid_tools, + force_tool_call=force_tool_call, + system=request_system_prompt, + ) + if dry_run: + yield request_data + return + + step_progression, step_metrics = self._step_checkpoint_llm_request_start(step_metrics, agent_step_span) + + invocation = llm_adapter.invoke_llm( + request_data=request_data, + messages=messages, + tools=valid_tools, + use_assistant_message=use_assistant_message, + requires_approval_tools=self.tool_rules_solver.get_requires_approval_tools( + set([t["name"] for t in valid_tools]) + ), + step_id=step_id, + actor=self.actor, + ) + async for chunk in invocation: + if llm_adapter.supports_token_streaming(): + if include_return_message_types is None or chunk.message_type in include_return_message_types: + first_chunk = True + yield chunk + # If you've reached this point without an error, break out of retry loop + break + except ValueError as e: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.invalid_llm_response.value) + raise e + except LLMError as e: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.llm_api_error.value) + raise e + except Exception as e: + if isinstance(e, ContextWindowExceededError) and llm_request_attempt < summarizer_settings.max_summarizer_retries: + # Retry case + messages = await self.summarize_conversation_history( + in_context_messages=messages, + new_letta_messages=self.response_messages, + force=True, + run_id=run_id, + step_id=step_id, + ) + else: + raise e + + step_progression, step_metrics = self._step_checkpoint_llm_request_finish( + step_metrics, agent_step_span, llm_adapter.llm_request_finish_timestamp_ns + ) + + self._update_global_usage_stats(llm_adapter.usage) + + # Handle the AI response with the extracted data + if tool_call is None and llm_adapter.tool_call is None: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.no_tool_call.value) + raise LLMError("No tool calls found in response, model must make a tool call") + + # TODO: how should be associate input messages with runs? + ## Set run_id on input messages before persisting + # if input_messages_to_persist and run_id: + # for message in input_messages_to_persist: + # if message.run_id is None: + # message.run_id = run_id + + persisted_messages, self.should_continue, self.stop_reason = await self._handle_ai_response( + tool_call or llm_adapter.tool_call, + [tool["name"] for tool in valid_tools], + self.agent_state, + self.tool_rules_solver, + UsageStatistics( + completion_tokens=self.usage.completion_tokens, + prompt_tokens=self.usage.prompt_tokens, + total_tokens=self.usage.total_tokens, + ), + reasoning_content=reasoning_content or llm_adapter.reasoning_content, + pre_computed_assistant_message_id=llm_adapter.message_id, + step_id=step_id, + initial_messages=input_messages_to_persist, + agent_step_span=agent_step_span, + is_final_step=(remaining_turns == 0), + run_id=run_id, + step_metrics=step_metrics, + is_approval=approval_response.approve if approval_response is not None else False, + is_denial=(approval_response.approve == False) if approval_response is not None else False, + denial_reason=approval_response.denial_reason if approval_response is not None else None, + ) + + new_message_idx = len(input_messages_to_persist) if input_messages_to_persist else 0 + self.response_messages.extend(persisted_messages[new_message_idx:]) + + if llm_adapter.supports_token_streaming(): + if persisted_messages[-1].role != "approval": + tool_return = [msg for msg in persisted_messages if msg.role == "tool"][-1].to_letta_messages()[0] + if not (use_assistant_message and tool_return.name == "send_message"): + if include_return_message_types is None or tool_return.message_type in include_return_message_types: + yield tool_return + else: + filter_user_messages = [m for m in persisted_messages[new_message_idx:] if m.role != "user"] + letta_messages = Message.to_letta_messages_from_list( + filter_user_messages, + use_assistant_message=use_assistant_message, + reverse=False, + ) + for message in letta_messages: + if include_return_message_types is None or message.message_type in include_return_message_types: + yield message + + # Persist approval responses immediately to prevent agent from getting into a bad state + if ( + len(input_messages_to_persist) == 1 + and input_messages_to_persist[0].role == "approval" + and persisted_messages[0].role == "approval" + and persisted_messages[1].role == "tool" + ): + self.agent_state.message_ids = self.agent_state.message_ids + [m.id for m in persisted_messages[:2]] + await self.agent_manager.update_message_ids_async( + agent_id=self.agent_state.id, message_ids=self.agent_state.message_ids, actor=self.actor + ) + step_progression, step_metrics = await self._step_checkpoint_finish(step_metrics, agent_step_span, logged_step) + except Exception as e: + caught_exception = e + self.logger.warning(f"Error during step processing: {e}") + self.job_update_metadata = {"error": str(e)} + + # This indicates we failed after we decided to stop stepping, which indicates a bug with our flow. + if not self.stop_reason: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + elif self.stop_reason.stop_reason in (StopReasonType.end_turn, StopReasonType.max_steps, StopReasonType.tool_rule): + self.logger.error("Error occurred during step processing, with valid stop reason: %s", self.stop_reason.stop_reason) + elif self.stop_reason.stop_reason not in ( + StopReasonType.no_tool_call, + StopReasonType.invalid_tool_call, + StopReasonType.invalid_llm_response, + StopReasonType.llm_api_error, + ): + self.logger.error("Error occurred during step processing, with unexpected stop reason: %s", self.stop_reason.stop_reason) + raise e + finally: + self.logger.debug("Running cleanup for agent loop run: %s", run_id) + self.logger.info("Running final update. Step Progression: %s", step_progression) + try: + if step_progression == StepProgression.FINISHED: + if not self.should_continue: + if self.stop_reason is None: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + if logged_step and step_id: + await self.step_manager.update_step_stop_reason(self.actor, step_id, self.stop_reason.stop_reason) + return + if step_progression < StepProgression.STEP_LOGGED: + # Error occurred before step was fully logged + import traceback + + if logged_step: + await self.step_manager.update_step_error_async( + actor=self.actor, + step_id=step_id, # Use original step_id for telemetry + error_type=type(caught_exception).__name__ if caught_exception is not None else "Unknown", + error_message=str(caught_exception) if caught_exception is not None else "Unknown error", + error_traceback=traceback.format_exc(), + stop_reason=self.stop_reason, + ) + if step_progression <= StepProgression.STREAM_RECEIVED: + if first_chunk and settings.track_errored_messages and input_messages_to_persist: + for message in input_messages_to_persist: + message.is_err = True + message.step_id = step_id + message.run_id = run_id + await self.message_manager.create_many_messages_async( + input_messages_to_persist, + actor=self.actor, + project_id=self.agent_state.project_id, + template_id=self.agent_state.template_id, + ) + elif step_progression <= StepProgression.LOGGED_TRACE: + if self.stop_reason is None: + self.logger.error("Error in step after logging step") + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + if logged_step: + await self.step_manager.update_step_stop_reason(self.actor, step_id, self.stop_reason.stop_reason) + else: + self.logger.error("Invalid StepProgression value") + + # Do tracking for failure cases. Can consolidate with success conditions later. + if settings.track_stop_reason: + await self._log_request(request_start_timestamp_ns, None, self.job_update_metadata, is_error=True, run_id=run_id) + + # Record partial step metrics on failure (capture whatever timing data we have) + if logged_step and step_metrics and step_progression < StepProgression.FINISHED: + # Calculate total step time up to the failure point + step_metrics.step_ns = get_utc_timestamp_ns() - step_metrics.step_start_ns + + await self._record_step_metrics( + step_id=step_id, + step_metrics=step_metrics, + run_id=run_id, + ) + except Exception as e: + self.logger.error(f"Error during post-completion step tracking: {e}") + + def _initialize_state(self): + self.should_continue = True + self.stop_reason = None + self.usage = LettaUsageStatistics() + self.last_step_usage: LettaUsageStatistics | None = None # Per-step usage for Step token details + self.job_update_metadata = None + self.last_function_response = None + self.response_messages = [] + self.override_system: str | None = None + + async def _check_credits(self) -> bool: + """Check if the organization still has credits. Returns True if OK or not configured.""" + try: + await self.credit_verification_service.verify_credits(self.actor.organization_id, self.agent_state.id) + return True + except InsufficientCreditsError: + self.logger.warning( + f"Insufficient credits for organization {self.actor.organization_id}, agent {self.agent_state.id}, stopping agent loop" + ) + return False + + @trace_method + async def _check_run_cancellation(self, run_id) -> bool: + try: + run = await self.run_manager.get_run_by_id(run_id=run_id, actor=self.actor) + return run.status == RunStatus.cancelled + except Exception as e: + # Log the error but don't fail the execution + self.logger.warning(f"Failed to check job cancellation status for job {run_id}: {e}") + return False + + @trace_method + async def _refresh_messages(self, in_context_messages: list[Message], force_system_prompt_refresh: bool = False): + """Refresh in-context messages. + + This performs two tasks: + 1) Rebuild the *system prompt* only if the memory/tool-rules/directories section has changed. + This avoids rebuilding the system prompt on every step due to dynamic metadata (e.g. message counts), + which can bust prefix caching. + 2) Scrub inner thoughts from messages. + + Args: + in_context_messages: Current in-context messages + force_system_prompt_refresh: If True, forces evaluation of whether the system prompt needs to be rebuilt. + (The rebuild will still be skipped if memory/tool-rules/directories haven't changed.) + + Returns: + Refreshed in-context messages. + """ + # Only rebuild when explicitly forced (e.g., after compaction). + # Normal turns should not trigger system prompt recompilation. + if force_system_prompt_refresh: + try: + in_context_messages = await self._rebuild_memory( + in_context_messages, + num_messages=None, + num_archival_memories=None, + force=True, + ) + except Exception: + raise + + # Always scrub inner thoughts regardless of system prompt refresh + in_context_messages = scrub_inner_thoughts_from_messages(in_context_messages, self.agent_state.llm_config) + return in_context_messages + + @trace_method + def generate_request_system_prompt( + self, + client_skills: list[ClientSkillSchema] | None, + current_system_message: Message, + ) -> str: + """Build request-scoped system prompt text without persisting request skills.""" + if self.override_system is not None: + # Request-scoped system overrides must pass through exactly as provided. + # Do not append compiled skills in this mode. + return self.override_system + + current_system_text = current_system_message.content[0].text + request_skills_block = self.agent_state.memory.compile_available_skills(client_skills=client_skills) + if not request_skills_block: + return current_system_text + return current_system_text.rstrip("\n") + "\n\n" + request_skills_block.lstrip("\n") + + @trace_method + async def _rebuild_memory( + self, + in_context_messages: list[Message], + num_messages: int | None, + num_archival_memories: int | None, + force: bool = False, + ): + agent_state = await self.agent_manager.refresh_memory_async(agent_state=self.agent_state, actor=self.actor) + + tool_constraint_block = None + if self.tool_rules_solver is not None: + tool_constraint_block = self.tool_rules_solver.compile_tool_rule_prompts() + + archive = await self.archive_manager.get_default_archive_for_agent_async( + agent_id=self.agent_state.id, + actor=self.actor, + ) + + if archive: + archive_tags = await self.passage_manager.get_unique_tags_for_archive_async( + archive_id=archive.id, + actor=self.actor, + ) + else: + archive_tags = None + + curr_system_message = in_context_messages[0] + curr_system_message_text = curr_system_message.content[0].text + + # refresh files + agent_state = await self.agent_manager.refresh_file_blocks(agent_state=agent_state, actor=self.actor) + + # generate memory string with current state + curr_memory_str = agent_state.memory.compile( + tool_usage_rules=tool_constraint_block, + sources=agent_state.sources, + max_files_open=agent_state.max_files_open, + llm_config=agent_state.llm_config, + ) + + # Skip rebuild unless explicitly forced and unless system/memory content actually changed. + system_prompt_changed = agent_state.system not in curr_system_message_text + memory_changed = curr_memory_str not in curr_system_message_text + if (not force) and (not system_prompt_changed) and (not memory_changed): + self.logger.debug( + f"Memory, sources, and system prompt haven't changed for agent id={agent_state.id} and actor=({self.actor.id}, {self.actor.name}), skipping system prompt rebuild" + ) + return in_context_messages + + memory_edit_timestamp = get_utc_time() + + # size of messages and archival memories + if num_messages is None: + num_messages = await self.message_manager.size_async(actor=self.actor, agent_id=agent_state.id) + if num_archival_memories is None: + num_archival_memories = await self.passage_manager.agent_passage_size_async(actor=self.actor, agent_id=agent_state.id) + + new_system_message_str = PromptGenerator.get_system_message_from_compiled_memory( + system_prompt=agent_state.system, + memory_with_sources=curr_memory_str, + agent_id=agent_state.id, + conversation_id=self.conversation_id or "default", + in_context_memory_last_edit=memory_edit_timestamp, + timezone=agent_state.timezone, + previous_message_count=num_messages - len(in_context_messages), + archival_memory_size=num_archival_memories, + archive_tags=archive_tags, + ) + + diff = united_diff(curr_system_message_text, new_system_message_str) + if len(diff) > 0: + self.logger.debug(f"Rebuilding system with new memory...\nDiff:\n{diff}") + + # [DB Call] Update Messages + new_system_message = await self.message_manager.update_message_by_id_async( + curr_system_message.id, message_update=MessageUpdate(content=new_system_message_str), actor=self.actor + ) + return [new_system_message, *in_context_messages[1:]] + + else: + return in_context_messages + + @trace_method + async def _get_valid_tools(self): + tools = self.agent_state.tools + valid_tool_names = self.tool_rules_solver.get_allowed_tool_names( + available_tools=set([t.name for t in tools]), + last_function_response=self.last_function_response, + error_on_empty=False, # Return empty list instead of raising error + ) or list(set(t.name for t in tools)) + allowed_tools = [ + enable_strict_mode(t.json_schema, strict=self.agent_state.llm_config.strict) for t in tools if t.name in set(valid_tool_names) + ] + terminal_tool_names = {rule.tool_name for rule in self.tool_rules_solver.terminal_tool_rules} + allowed_tools = runtime_override_tool_json_schema( + tool_list=allowed_tools, + response_format=self.agent_state.response_format, + request_heartbeat=True, + terminal_tools=terminal_tool_names, + ) + return allowed_tools + + @trace_method + def _request_checkpoint_start(self, request_start_timestamp_ns: int | None) -> Span | None: + if request_start_timestamp_ns is not None: + request_span = tracer.start_span("time_to_first_token", start_time=request_start_timestamp_ns) + request_span.set_attributes( + {f"llm_config.{k}": v for k, v in self.agent_state.llm_config.model_dump().items() if v is not None} + ) + return request_span + return None + + @trace_method + def _request_checkpoint_ttft(self, request_span: Span | None, request_start_timestamp_ns: int | None) -> Span | None: + if request_span: + ttft_ns = get_utc_timestamp_ns() - request_start_timestamp_ns + request_span.add_event(name="time_to_first_token_ms", attributes={"ttft_ms": ns_to_ms(ttft_ns)}) + return request_span + return None + + @trace_method + async def _request_checkpoint_finish( + self, request_span: Span | None, request_start_timestamp_ns: int | None, run_id: str | None + ) -> None: + await self._log_request(request_start_timestamp_ns, request_span, self.job_update_metadata, is_error=False, run_id=run_id) + return None + + @trace_method + async def _step_checkpoint_start(self, step_id: str, run_id: str | None) -> Tuple[StepProgression, Step, StepMetrics, Span]: + step_start_ns = get_utc_timestamp_ns() + step_metrics = StepMetrics(id=step_id, step_start_ns=step_start_ns) + agent_step_span = tracer.start_span("agent_step", start_time=step_start_ns) + agent_step_span.set_attributes({"step_id": step_id}) + # Create step early with PENDING status + logged_step = await self.step_manager.log_step_async( + actor=self.actor, + agent_id=self.agent_state.id, + provider_name=self.agent_state.llm_config.model_endpoint_type, + provider_category=self.agent_state.llm_config.provider_category or "base", + model=self.agent_state.llm_config.model, + model_endpoint=self.agent_state.llm_config.model_endpoint, + context_window_limit=self.agent_state.llm_config.context_window, + usage=UsageStatistics(completion_tokens=0, prompt_tokens=0, total_tokens=0), + provider_id=None, + run_id=run_id, + step_id=step_id, + project_id=self.agent_state.project_id, + status=StepStatus.PENDING, + model_handle=self.agent_state.llm_config.handle, + ) + + # Also create step metrics early and update at the end of the step + self._record_step_metrics(step_id=step_id, step_metrics=step_metrics, run_id=run_id) + return StepProgression.START, logged_step, step_metrics, agent_step_span + + @trace_method + def _step_checkpoint_llm_request_start(self, step_metrics: StepMetrics, agent_step_span: Span) -> Tuple[StepProgression, StepMetrics]: + llm_request_start_ns = get_utc_timestamp_ns() + step_metrics.llm_request_start_ns = llm_request_start_ns + agent_step_span.add_event( + name="request_start_to_provider_request_start_ns", + attributes={"request_start_to_provider_request_start_ns": ns_to_ms(llm_request_start_ns)}, + ) + return StepProgression.START, step_metrics + + @trace_method + def _step_checkpoint_llm_request_finish( + self, step_metrics: StepMetrics, agent_step_span: Span, llm_request_finish_timestamp_ns: int + ) -> Tuple[StepProgression, StepMetrics]: + llm_request_ns = llm_request_finish_timestamp_ns - step_metrics.llm_request_start_ns + step_metrics.llm_request_ns = llm_request_ns + agent_step_span.add_event(name="llm_request_ms", attributes={"duration_ms": ns_to_ms(llm_request_ns)}) + return StepProgression.RESPONSE_RECEIVED, step_metrics + + @trace_method + async def _step_checkpoint_finish( + self, step_metrics: StepMetrics, agent_step_span: Span | None, logged_step: Step | None + ) -> Tuple[StepProgression, StepMetrics]: + if step_metrics.step_start_ns: + step_ns = get_utc_timestamp_ns() - step_metrics.step_start_ns + step_metrics.step_ns = step_ns + if agent_step_span is not None: + agent_step_span.add_event(name="step_ms", attributes={"duration_ms": ns_to_ms(step_ns)}) + agent_step_span.end() + self._record_step_metrics(step_id=step_metrics.id, step_metrics=step_metrics) + + # Update step with actual usage now that we have it (if step was created) + if logged_step: + # Use per-step usage for Step token details (not accumulated self.usage) + # Each Step should store its own per-step values, not accumulated totals + step_usage = self.last_step_usage if self.last_step_usage else self.usage + + # Build detailed token breakdowns from per-step LettaUsageStatistics + # Use `is not None` to capture 0 values (meaning "provider reported 0 cached/reasoning tokens") + # Only include fields that were actually reported by the provider + prompt_details = None + if step_usage.cached_input_tokens is not None or step_usage.cache_write_tokens is not None: + prompt_details = UsageStatisticsPromptTokenDetails( + cached_tokens=step_usage.cached_input_tokens if step_usage.cached_input_tokens is not None else None, + cache_read_tokens=step_usage.cached_input_tokens if step_usage.cached_input_tokens is not None else None, + cache_creation_tokens=step_usage.cache_write_tokens if step_usage.cache_write_tokens is not None else None, + ) + + completion_details = None + if step_usage.reasoning_tokens is not None: + completion_details = UsageStatisticsCompletionTokenDetails( + reasoning_tokens=step_usage.reasoning_tokens, + ) + + await self.step_manager.update_step_success_async( + self.actor, + step_metrics.id, + UsageStatistics( + completion_tokens=step_usage.completion_tokens, + prompt_tokens=step_usage.prompt_tokens, + total_tokens=step_usage.total_tokens, + prompt_tokens_details=prompt_details, + completion_tokens_details=completion_details, + ), + self.stop_reason, + ) + return StepProgression.FINISHED, step_metrics + + def _update_global_usage_stats(self, step_usage_stats: LettaUsageStatistics): + # Save per-step usage for Step token details (before accumulating) + self.last_step_usage = step_usage_stats + + # For newer agent loops (e.g. V3), we also maintain a running + # estimate of the current context size derived from the latest + # step's total tokens. This can then be safely adjusted after + # summarization without mutating the historical per-step usage + # stored in Step metrics. + if hasattr(self, "context_token_estimate"): + self.context_token_estimate = step_usage_stats.total_tokens + + # Accumulate into global usage + self.usage.step_count += step_usage_stats.step_count + self.usage.completion_tokens += step_usage_stats.completion_tokens + self.usage.prompt_tokens += step_usage_stats.prompt_tokens + self.usage.total_tokens += step_usage_stats.total_tokens + # Aggregate cache and reasoning token fields (handle None values) + if step_usage_stats.cached_input_tokens is not None: + self.usage.cached_input_tokens = (self.usage.cached_input_tokens or 0) + step_usage_stats.cached_input_tokens + if step_usage_stats.cache_write_tokens is not None: + self.usage.cache_write_tokens = (self.usage.cache_write_tokens or 0) + step_usage_stats.cache_write_tokens + if step_usage_stats.reasoning_tokens is not None: + self.usage.reasoning_tokens = (self.usage.reasoning_tokens or 0) + step_usage_stats.reasoning_tokens + + @trace_method + async def _handle_ai_response( + self, + tool_call: ToolCall, + valid_tool_names: list[str], + agent_state: AgentState, + tool_rules_solver: ToolRulesSolver, + usage: UsageStatistics, + reasoning_content: list[TextContent | ReasoningContent | RedactedReasoningContent | OmittedReasoningContent] | None = None, + pre_computed_assistant_message_id: str | None = None, + step_id: str | None = None, + initial_messages: list[Message] | None = None, + agent_step_span: Span | None = None, + is_final_step: bool | None = None, + run_id: str | None = None, + step_metrics: StepMetrics = None, + is_approval: bool | None = None, + is_denial: bool | None = None, + denial_reason: str | None = None, + ) -> tuple[list[Message], bool, LettaStopReason | None]: + """ + Handle the final AI response once streaming completes, execute / validate the + tool call, decide whether we should keep stepping, and persist state. + """ + tool_call_id: str = tool_call.id or f"call_{uuid.uuid4().hex[:8]}" + + if is_denial: + continue_stepping = True + stop_reason = None + tool_call_messages = create_letta_messages_from_llm_response( + agent_id=agent_state.id, + model=agent_state.llm_config.model, + function_name=tool_call.function.name, + function_arguments={}, + tool_execution_result=ToolExecutionResult(status="error"), + tool_call_id=tool_call_id, + function_response=f"Error: request to call tool denied. User reason: {denial_reason}", + timezone=agent_state.timezone, + continue_stepping=continue_stepping, + heartbeat_reason=f"{NON_USER_MSG_PREFIX}Continuing: user denied request to call tool.", + reasoning_content=None, + pre_computed_assistant_message_id=None, + step_id=step_id, + is_approval_response=True, + run_id=run_id, + ) + messages_to_persist = (initial_messages or []) + tool_call_messages + + for message in messages_to_persist: + message.step_id = step_id + message.run_id = run_id + + persisted_messages = await self.message_manager.create_many_messages_async( + messages_to_persist, + actor=self.actor, + run_id=run_id, + project_id=agent_state.project_id, + template_id=agent_state.template_id, + ) + return persisted_messages, continue_stepping, stop_reason + + # 1. Parse and validate the tool-call envelope + tool_call_name: str = tool_call.function.name + + tool_args = _safe_load_tool_call_str(tool_call.function.arguments) + request_heartbeat: bool = _pop_heartbeat(tool_args) + tool_args.pop(INNER_THOUGHTS_KWARG, None) + + log_telemetry( + self.logger, + "_handle_ai_response execute tool start", + tool_name=tool_call_name, + tool_args=tool_args, + tool_call_id=tool_call_id, + request_heartbeat=request_heartbeat, + ) + + if not is_approval and tool_rules_solver.is_requires_approval_tool(tool_call_name): + tool_args[REQUEST_HEARTBEAT_PARAM] = request_heartbeat + approval_messages = create_approval_request_message_from_llm_response( + agent_id=agent_state.id, + model=agent_state.llm_config.model, + requested_tool_calls=[ + ToolCall(id=tool_call_id, function=FunctionCall(name=tool_call_name, arguments=json.dumps(tool_args))) + ], + reasoning_content=reasoning_content, + pre_computed_assistant_message_id=pre_computed_assistant_message_id, + step_id=step_id, + run_id=run_id, + ) + messages_to_persist = (initial_messages or []) + approval_messages + continue_stepping = False + stop_reason = LettaStopReason(stop_reason=StopReasonType.requires_approval.value) + else: + # 2. Execute the tool (or synthesize an error result if disallowed) + tool_rule_violated = tool_call_name not in valid_tool_names and not is_approval + if tool_rule_violated: + tool_execution_result = _build_rule_violation_result(tool_call_name, valid_tool_names, tool_rules_solver) + else: + # Track tool execution time + tool_start_time = get_utc_timestamp_ns() + target_tool = next((x for x in agent_state.tools if x.name == tool_call_name), None) + + tool_execution_result = await self._execute_tool( + target_tool=target_tool, + tool_args=tool_args, + agent_state=agent_state, + agent_step_span=agent_step_span, + step_id=step_id, + ) + tool_end_time = get_utc_timestamp_ns() + + # Store tool execution time in metrics + step_metrics.tool_execution_ns = tool_end_time - tool_start_time + + log_telemetry( + self.logger, + "_handle_ai_response execute tool finish", + tool_execution_result=tool_execution_result, + tool_call_id=tool_call_id, + ) + + # 3. Prepare the function-response payload + truncate = tool_call_name not in {"conversation_search", "conversation_search_date", "archival_memory_search"} + return_char_limit = next( + (t.return_char_limit for t in agent_state.tools if t.name == tool_call_name), + None, + ) + function_response_string = validate_function_response( + tool_execution_result.func_return, + return_char_limit=return_char_limit, + truncate=truncate, + ) + self.last_function_response = package_function_response( + was_success=tool_execution_result.success_flag, + response_string=function_response_string, + timezone=agent_state.timezone, + ) + + # 4. Decide whether to keep stepping (focal section simplified) + continue_stepping, heartbeat_reason, stop_reason = self._decide_continuation( + agent_state=agent_state, + request_heartbeat=request_heartbeat, + tool_call_name=tool_call_name, + tool_rule_violated=tool_rule_violated, + tool_rules_solver=tool_rules_solver, + is_final_step=is_final_step, + ) + + # 5. Create messages (step was already created at the beginning) + tool_call_messages = create_letta_messages_from_llm_response( + agent_id=agent_state.id, + model=agent_state.llm_config.model, + function_name=tool_call_name, + function_arguments=tool_args, + tool_execution_result=tool_execution_result, + tool_call_id=tool_call_id, + function_response=function_response_string, + timezone=agent_state.timezone, + continue_stepping=continue_stepping, + heartbeat_reason=heartbeat_reason, + reasoning_content=reasoning_content, + pre_computed_assistant_message_id=pre_computed_assistant_message_id, + step_id=step_id, + run_id=run_id, + is_approval_response=is_approval or is_denial, + ) + messages_to_persist = (initial_messages or []) + tool_call_messages + + for message in messages_to_persist: + message.step_id = step_id + message.run_id = run_id + + persisted_messages = await self.message_manager.create_many_messages_async( + messages_to_persist, actor=self.actor, run_id=run_id, project_id=agent_state.project_id, template_id=agent_state.template_id + ) + + return persisted_messages, continue_stepping, stop_reason + + @trace_method + def _decide_continuation( + self, + agent_state: AgentState, + request_heartbeat: bool, + tool_call_name: str, + tool_rule_violated: bool, + tool_rules_solver: ToolRulesSolver, + is_final_step: bool | None, + ) -> tuple[bool, str | None, LettaStopReason | None]: + continue_stepping = request_heartbeat + heartbeat_reason: str | None = None + stop_reason: LettaStopReason | None = None + + if tool_rule_violated: + continue_stepping = True + heartbeat_reason = f"{NON_USER_MSG_PREFIX}Continuing: tool rule violation." + else: + tool_rules_solver.register_tool_call(tool_call_name) + + if tool_rules_solver.is_terminal_tool(tool_call_name): + if continue_stepping: + stop_reason = LettaStopReason(stop_reason=StopReasonType.tool_rule.value) + continue_stepping = False + + elif tool_rules_solver.has_children_tools(tool_call_name): + continue_stepping = True + heartbeat_reason = f"{NON_USER_MSG_PREFIX}Continuing: child tool rule." + + elif tool_rules_solver.is_continue_tool(tool_call_name): + continue_stepping = True + heartbeat_reason = f"{NON_USER_MSG_PREFIX}Continuing: continue tool rule." + + # – hard stop overrides – + if is_final_step: + continue_stepping = False + stop_reason = LettaStopReason(stop_reason=StopReasonType.max_steps.value) + else: + uncalled = tool_rules_solver.get_uncalled_required_tools(available_tools=set([t.name for t in agent_state.tools])) + if not continue_stepping and uncalled: + continue_stepping = True + heartbeat_reason = f"{NON_USER_MSG_PREFIX}Continuing, user expects these tools: [{', '.join(uncalled)}] to be called still." + + stop_reason = None # reset – we’re still going + + return continue_stepping, heartbeat_reason, stop_reason + + @trace_method + async def _execute_tool( + self, + target_tool: Tool, + tool_args: JsonDict, + agent_state: AgentState, + agent_step_span: Span | None = None, + step_id: str | None = None, + ) -> "ToolExecutionResult": + """ + Executes a tool and returns the ToolExecutionResult. + """ + from letta.schemas.tool_execution_result import ToolExecutionResult + + # Check for None before accessing attributes + if not target_tool: + return ToolExecutionResult( + func_return="Tool not found", + status="error", + ) + + tool_name = target_tool.name + + # TODO: This temp. Move this logic and code to executors + + if agent_step_span: + start_time = get_utc_timestamp_ns() + agent_step_span.add_event(name="tool_execution_started") + + # Use pre-decrypted environment variable values (populated in from_orm_async) + sandbox_env_vars = {var.key: var.value or "" for var in agent_state.secrets} + tool_execution_manager = ToolExecutionManager( + agent_state=agent_state, + message_manager=self.message_manager, + run_manager=self.run_manager, + agent_manager=self.agent_manager, + block_manager=self.block_manager, + passage_manager=self.passage_manager, + sandbox_env_vars=sandbox_env_vars, + actor=self.actor, + ) + # TODO: Integrate sandbox result + log_event(name=f"start_{tool_name}_execution", attributes=tool_args) + tool_execution_result = await tool_execution_manager.execute_tool_async( + function_name=tool_name, + function_args=tool_args, + tool=target_tool, + step_id=step_id, + ) + if agent_step_span: + end_time = get_utc_timestamp_ns() + agent_step_span.add_event( + name="tool_execution_completed", + attributes={ + "tool_name": target_tool.name, + "duration_ms": ns_to_ms(end_time - start_time), + "success": tool_execution_result.success_flag, + "tool_type": target_tool.tool_type, + "tool_id": target_tool.id, + }, + ) + log_event(name=f"finish_{tool_name}_execution", attributes=tool_execution_result.model_dump()) + return tool_execution_result + + @trace_method + async def summarize_conversation_history( + self, + in_context_messages: list[Message], + new_letta_messages: list[Message], + total_tokens: int | None = None, + force: bool = False, + run_id: str | None = None, + step_id: str | None = None, + ) -> list[Message]: + self.logger.warning("Running deprecated v2 summarizer. This should be removed in the future.") + # always skip summarization if last message is an approval request message + skip_summarization = False + latest_messages = in_context_messages + new_letta_messages + if latest_messages[-1].role == "approval" and len(latest_messages[-1].tool_calls) > 0: + skip_summarization = True + + # If total tokens is reached, we truncate down + # TODO: This can be broken by bad configs, e.g. lower bound too high, initial messages too fat, etc. + # TODO: `force` and `clear` seem to no longer be used, we should remove + if not skip_summarization: + try: + if force or (total_tokens and total_tokens > self.agent_state.llm_config.context_window): + self.logger.warning( + f"Total tokens {total_tokens} exceeds configured max tokens {self.agent_state.llm_config.context_window}, forcefully clearing message history." + ) + new_in_context_messages, _updated = await self.summarizer.summarize( + in_context_messages=in_context_messages, + new_letta_messages=new_letta_messages, + force=True, + clear=True, + run_id=run_id, + step_id=step_id, + ) + else: + # NOTE (Sarah): Seems like this is doing nothing? + self.logger.info( + f"Total tokens {total_tokens} does not exceed configured max tokens {self.agent_state.llm_config.context_window}, passing summarizing w/o force." + ) + new_in_context_messages, _updated = await self.summarizer.summarize( + in_context_messages=in_context_messages, + new_letta_messages=new_letta_messages, + run_id=run_id, + step_id=step_id, + ) + except Exception as e: + self.logger.error(f"Failed to summarize conversation history: {e}") + new_in_context_messages = in_context_messages + new_letta_messages + else: + new_in_context_messages = in_context_messages + new_letta_messages + + message_ids = [m.id for m in new_in_context_messages] + await self.agent_manager.update_message_ids_async( + agent_id=self.agent_state.id, + message_ids=message_ids, + actor=self.actor, + ) + self.agent_state.message_ids = message_ids + + return new_in_context_messages + + def _record_step_metrics( + self, + *, + step_id: str, + step_metrics: StepMetrics, + run_id: str | None = None, + ): + task = safe_create_task( + self.step_manager.record_step_metrics_async( + actor=self.actor, + step_id=step_id, + llm_request_ns=step_metrics.llm_request_ns, + tool_execution_ns=step_metrics.tool_execution_ns, + step_ns=step_metrics.step_ns, + agent_id=self.agent_state.id, + run_id=run_id, + project_id=self.agent_state.project_id, + template_id=self.agent_state.template_id, + base_template_id=self.agent_state.base_template_id, + ), + label="record_step_metrics", + ) + return task + + @trace_method + async def _log_request( + self, + request_start_timestamp_ns: int, + request_span: "Span | None", + job_update_metadata: dict | None, + is_error: bool, + run_id: str | None = None, + ): + if request_start_timestamp_ns: + now_ns, now = get_utc_timestamp_ns(), get_utc_time() + duration_ns = now_ns - request_start_timestamp_ns + if request_span: + request_span.add_event(name="letta_request_ms", attributes={"duration_ms": ns_to_ms(duration_ns)}) + await self._update_agent_last_run_metrics(now, ns_to_ms(duration_ns)) + # if settings.track_agent_run and run_id: + # await self.job_manager.record_response_duration(run_id, duration_ns, self.actor) + # await self.job_manager.safe_update_job_status_async( + # job_id=run_id, + # new_status=JobStatus.failed if is_error else JobStatus.completed, + # actor=self.actor, + # stop_reason=self.stop_reason.stop_reason if self.stop_reason else StopReasonType.error, + # metadata=job_update_metadata, + # ) + if request_span: + request_span.end() + + @trace_method + async def _update_agent_last_run_metrics(self, completion_time: datetime, duration_ms: float) -> None: + if not settings.track_last_agent_run: + return + try: + await self.agent_manager.update_agent_async( + agent_id=self.agent_state.id, + agent_update=UpdateAgent(last_run_completion=completion_time, last_run_duration_ms=duration_ms), + actor=self.actor, + ) + except Exception as e: + self.logger.error(f"Failed to update agent's last run metrics: {e}") + + def get_finish_chunks_for_stream( + self, + usage: LettaUsageStatistics, + stop_reason: LettaStopReason | None = None, + ): + if stop_reason is None: + stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + return [ + stop_reason.model_dump_json(), + usage.model_dump_json(), + MessageStreamStatus.done.value, + ] diff --git a/letta/agents/letta_agent_v3.py b/letta/agents/letta_agent_v3.py new file mode 100644 index 0000000..8dabe57 --- /dev/null +++ b/letta/agents/letta_agent_v3.py @@ -0,0 +1,2134 @@ +import asyncio +import json +import uuid +from typing import Any, AsyncGenerator, Dict, Optional + +from opentelemetry.trace import Span + +from letta.adapters.letta_llm_adapter import LettaLLMAdapter +from letta.adapters.sglang_native_adapter import SGLangNativeAdapter +from letta.adapters.simple_llm_request_adapter import SimpleLLMRequestAdapter +from letta.adapters.simple_llm_stream_adapter import SimpleLLMStreamAdapter +from letta.agents.helpers import ( + _build_rule_violation_result, + _load_last_function_response, + _maybe_get_approval_messages, + _maybe_get_pending_tool_call_message, + _prepare_in_context_messages_no_persist_async, + _safe_load_tool_call_str, + generate_step_id, + merge_and_validate_prefilled_args, +) +from letta.agents.letta_agent_v2 import LettaAgentV2 +from letta.constants import DEFAULT_MAX_STEPS, NON_USER_MSG_PREFIX, REQUEST_HEARTBEAT_PARAM +from letta.errors import ( + ContextWindowExceededError, + LLMEmptyResponseError, + LLMError, + LLMProviderOverloaded, + LLMRateLimitError, + LLMServerError, + SystemPromptTokenExceededError, +) +from letta.helpers import ToolRulesSolver +from letta.helpers.datetime_helpers import get_utc_time, get_utc_timestamp_ns +from letta.helpers.tool_execution_helper import enable_strict_mode +from letta.llm_api.llm_client import LLMClient +from letta.local_llm.constants import INNER_THOUGHTS_KWARG +from letta.otel.tracing import trace_method +from letta.schemas.agent import AgentState +from letta.schemas.enums import LLMCallType +from letta.schemas.letta_message import ( + ApprovalReturn, + CompactionStats, + EventMessage, + LettaErrorMessage, + LettaMessage, + MessageType, + SummaryMessage, + extract_compaction_stats_from_packed_json, +) +from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, RedactedReasoningContent, TextContent +from letta.schemas.letta_request import ClientSkillSchema, ClientToolSchema +from letta.schemas.letta_response import LettaResponse, TurnTokenData +from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType +from letta.schemas.message import Message, MessageCreate, ToolReturn +from letta.schemas.openai.chat_completion_response import ChoiceLogprobs, ToolCall, ToolCallDenial, UsageStatistics +from letta.schemas.provider_trace import BillingContext +from letta.schemas.step import StepProgression +from letta.schemas.step_metrics import StepMetrics +from letta.schemas.tool_execution_result import ToolExecutionResult +from letta.schemas.user import User +from letta.server.rest_api.utils import ( + create_approval_request_message_from_llm_response, + create_letta_messages_from_llm_response, + create_parallel_tool_messages_from_llm_response, + create_tool_returns_for_denials, +) +from letta.services.conversation_manager import ConversationManager +from letta.services.helpers.tool_parser_helper import runtime_override_tool_json_schema +from letta.services.llm_router import get_llm_routing_client +from letta.services.provider_manager import AUTO_MODE_HANDLES +from letta.services.summarizer.compact import compact_messages +from letta.services.summarizer.summarizer_config import CompactionSettings +from letta.services.summarizer.summarizer_sliding_window import count_tokens +from letta.services.summarizer.thresholds import get_compaction_trigger_threshold +from letta.settings import settings, summarizer_settings +from letta.system import package_function_response +from letta.utils import safe_create_task_with_return, validate_function_response + + +def extract_compaction_stats_from_message(message: Message) -> CompactionStats | None: + """ + Extract CompactionStats from a Message object's packed content. + + Args: + message: Message object with packed JSON content + + Returns: + CompactionStats if found and valid, None otherwise + """ + try: + if message.content and len(message.content) == 1: + text_content = message.content[0].text + return extract_compaction_stats_from_packed_json(text_content) + except AttributeError: + pass + return None + + +class LettaAgentV3(LettaAgentV2): + """ + Similar to V2, but stripped down / simplified, while also generalized: + * Supports non-tool returns + * No inner thoughts in kwargs + * No heartbeats (loops happen on tool calls) + + TODOs: + * Support tool rules + * Support Gemini / OpenAI client + """ + + def __init__( + self, + agent_state: AgentState, + actor: User, + conversation_id: str | None = None, + ): + super().__init__(agent_state, actor) + # Set conversation_id after parent init (which calls _initialize_state) + self.conversation_id = conversation_id + + def _initialize_state(self): + super()._initialize_state() + self._require_tool_call = False + # Approximate token count for the *current* in-context buffer, used + # only for proactive summarization / eviction logic. This is derived + # from per-step usage but can be updated after summarization without + # affecting step-level telemetry. + self.context_token_estimate: int | None = None + self.in_context_messages: list[Message] = [] # in-memory tracker + # Conversation mode: when set, messages are tracked per-conversation + self.conversation_id: str | None = None + # Client-side tools passed in the request (executed by client, not server) + self.client_tools: list[ClientToolSchema] = [] + # Client-side skills passed in the request (rendered in system prompt) + self.client_skills: list[ClientSkillSchema] = [] + # Log probabilities from the most recent LLM call (for RL training) + self.logprobs: ChoiceLogprobs | None = None + # Multi-turn token tracking for RL training (accumulated across all LLM calls) + self.turns: list[TurnTokenData] = [] + self.return_token_ids: bool = False + + def _compute_tool_return_truncation_chars(self) -> int: + """Compute a dynamic cap for tool returns in requests. + + Heuristic: ~20% of context window × 4 chars/token, minimum 5k chars. + This prevents any single tool return from consuming too much context. + """ + try: + cap = int(self.agent_state.llm_config.context_window * 0.2 * 4) # 20% of tokens → chars + except Exception: + cap = 5000 + return max(5000, cap) + + @trace_method + async def build_request( + self, + input_messages: list[MessageCreate], + client_skills: list[ClientSkillSchema] | None = None, + client_tools: list[ClientToolSchema] | None = None, + conversation_id: str | None = None, + override_system: str | None = None, + ) -> dict: + """ + Build the request data for an LLM call without actually executing it. + + Overrides V2 to support conversation-scoped messages, conversation-isolated + blocks, and client-side tools — matching the real execution path in step(). + + Args: + input_messages: List of new messages to process + client_skills: Optional client-side skills to include in system prompt + client_tools: Optional client-side tools to merge into tool list + conversation_id: Optional conversation ID for conversation-scoped context + + Returns: + dict: The request data that would be sent to the LLM + """ + from letta.adapters.letta_llm_request_adapter import LettaLLMRequestAdapter + + self._initialize_state() + self.client_tools = client_tools or [] + self.client_skills = client_skills or [] + self.override_system = override_system + self.conversation_id = conversation_id + + # Apply conversation-specific block overrides (same as step()) + if conversation_id: + self.agent_state = await ConversationManager().apply_isolated_blocks_to_agent_state( + agent_state=self.agent_state, + conversation_id=conversation_id, + actor=self.actor, + ) + + in_context_messages, input_messages_to_persist = await _prepare_in_context_messages_no_persist_async( + input_messages, self.agent_state, self.message_manager, self.actor, None, conversation_id=conversation_id + ) + + response = self._step( + run_id=None, + messages=in_context_messages + input_messages_to_persist, + llm_adapter=LettaLLMRequestAdapter( + llm_client=self.llm_client, + llm_config=self.agent_state.llm_config, + call_type=LLMCallType.agent_step, + agent_id=self.agent_state.id, + agent_tags=self.agent_state.tags, + org_id=self.actor.organization_id, + user_id=self.actor.id, + ), + dry_run=True, + enforce_run_id_set=False, + ) + request = {} + async for chunk in response: + request = chunk + break + + return request + + @trace_method + async def step( + self, + input_messages: list[MessageCreate], + max_steps: int = DEFAULT_MAX_STEPS, + run_id: str | None = None, + use_assistant_message: bool = True, # NOTE: not used + include_return_message_types: list[MessageType] | None = None, + request_start_timestamp_ns: int | None = None, + conversation_id: str | None = None, + client_tools: list[ClientToolSchema] | None = None, + client_skills: list[ClientSkillSchema] | None = None, + override_system: str | None = None, + include_compaction_messages: bool = False, + billing_context: "BillingContext | None" = None, + ) -> LettaResponse: + """ + Execute the agent loop in blocking mode, returning all messages at once. + + Args: + input_messages: List of new messages to process + max_steps: Maximum number of agent steps to execute + run_id: Optional job/run ID for tracking + use_assistant_message: Whether to use assistant message format + include_return_message_types: Filter for which message types to return + request_start_timestamp_ns: Start time for tracking request duration + conversation_id: Optional conversation ID for conversation-scoped messaging + client_tools: Optional list of client-side tools. When called, execution pauses + for client to provide tool returns. + include_compaction_messages: Whether to include SummaryMessage/EventMessage in response + and use role=summary for stored summary messages. + + Returns: + LettaResponse: Complete response with all messages and metadata + """ + self._initialize_state() + self.conversation_id = conversation_id + self.client_tools = client_tools or [] + self.client_skills = client_skills or [] + self.override_system = override_system + + # Apply conversation-specific block overrides if conversation_id is provided + if conversation_id: + self.agent_state = await ConversationManager().apply_isolated_blocks_to_agent_state( + agent_state=self.agent_state, + conversation_id=conversation_id, + actor=self.actor, + ) + + request_span = self._request_checkpoint_start(request_start_timestamp_ns=request_start_timestamp_ns) + response_letta_messages = [] + + # Prepare in-context messages (conversation mode if conversation_id provided) + curr_in_context_messages, input_messages_to_persist = await _prepare_in_context_messages_no_persist_async( + input_messages, + self.agent_state, + self.message_manager, + self.actor, + run_id, + conversation_id=conversation_id, + ) + follow_up_messages = [] + if len(input_messages_to_persist) > 1 and input_messages_to_persist[0].role == "approval": + follow_up_messages = input_messages_to_persist[1:] + input_messages_to_persist = [input_messages_to_persist[0]] + + self.in_context_messages = curr_in_context_messages + + # Check if we should use SGLang native adapter for multi-turn RL training. + # Matches handles starting with "sglang/" OR providers named like "*sglang*" + # (e.g. "slime-sglang" used in training). + _handle = self.agent_state.llm_config.handle or "" + _provider = (self.agent_state.llm_config.provider_name or "").lower() + use_sglang_native = ( + self.agent_state.llm_config.return_token_ids and _handle and (_handle.startswith("sglang/") or "sglang" in _provider) + ) + self.return_token_ids = use_sglang_native + + if use_sglang_native: + # Use SGLang native adapter for multi-turn RL training + llm_adapter = SGLangNativeAdapter( + llm_client=self.llm_client, + llm_config=self.agent_state.llm_config, + model_settings=self.agent_state.model_settings, + call_type=LLMCallType.agent_step, + agent_id=self.agent_state.id, + agent_tags=self.agent_state.tags, + run_id=run_id, + org_id=self.actor.organization_id, + user_id=self.actor.id, + ) + # Reset turns tracking for this step + self.turns = [] + else: + llm_adapter = SimpleLLMRequestAdapter( + llm_client=self.llm_client, + llm_config=self.agent_state.llm_config, + call_type=LLMCallType.agent_step, + agent_id=self.agent_state.id, + agent_tags=self.agent_state.tags, + run_id=run_id, + org_id=self.actor.organization_id, + user_id=self.actor.id, + billing_context=billing_context, + ) + + credit_task = None + for i in range(max_steps): + if i == 1 and follow_up_messages: + input_messages_to_persist = follow_up_messages + follow_up_messages = [] + + # Await credit check from previous iteration before running next step + if credit_task is not None: + if not await credit_task: + self.should_continue = False + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.insufficient_credits) + break + credit_task = None + + response = self._step( + # we append input_messages_to_persist since they aren't checkpointed as in-context until the end of the step (may be rolled back) + messages=list(self.in_context_messages + input_messages_to_persist), + input_messages_to_persist=input_messages_to_persist, + llm_adapter=llm_adapter, + run_id=run_id, + # use_assistant_message=use_assistant_message, + include_return_message_types=include_return_message_types, + request_start_timestamp_ns=request_start_timestamp_ns, + include_compaction_messages=include_compaction_messages, + billing_context=billing_context, + ) + input_messages_to_persist = [] # clear after first step + + async for chunk in response: + response_letta_messages.append(chunk) + + # Check if step was cancelled - break out of the step loop + if not self.should_continue and self.stop_reason.stop_reason == StopReasonType.cancelled.value: + break + + # TODO: persist the input messages if successful first step completion + # TODO: persist the new messages / step / run + + ## Proactive summarization if approaching context limit + # if ( + # self.context_token_estimate is not None + # and self.context_token_estimate > self.agent_state.llm_config.context_window * SUMMARIZATION_TRIGGER_MULTIPLIER + # and not self.agent_state.message_buffer_autoclear + # ): + # self.logger.warning( + # f"Step usage ({self.last_step_usage.total_tokens} tokens) approaching " + # f"context limit ({self.agent_state.llm_config.context_window}), triggering summarization." + # ) + + # in_context_messages = await self.summarize_conversation_history( + # in_context_messages=in_context_messages, + # new_letta_messages=self.response_messages, + # total_tokens=self.context_token_estimate, + # force=True, + # ) + + # # Clear to avoid duplication in next iteration + # self.response_messages = [] + + if not self.should_continue: + break + + # Fire credit check to run in parallel with loop overhead / next step setup + credit_task = safe_create_task_with_return(self._check_credits()) + + # input_messages_to_persist = [] + + if i == max_steps - 1 and self.stop_reason is None: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.max_steps.value) + + ## Rebuild context window after stepping (safety net) + # if not self.agent_state.message_buffer_autoclear: + # if self.context_token_estimate is not None: + # await self.summarize_conversation_history( + # in_context_messages=in_context_messages, + # new_letta_messages=self.response_messages, + # total_tokens=self.context_token_estimate, + # force=False, + # ) + # else: + # self.logger.warning( + # "Post-loop summarization skipped: last_step_usage is None. " + # "No step completed successfully or usage stats were not updated." + # ) + + if self.stop_reason is None: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + + # construct the response + response_letta_messages = Message.to_letta_messages_from_list( + self.response_messages, + use_assistant_message=False, # NOTE: set to false + reverse=False, + text_is_assistant_message=True, + ) + if include_return_message_types: + response_letta_messages = [m for m in response_letta_messages if m.message_type in include_return_message_types] + # Set context_tokens to expose actual context window usage (vs accumulated prompt_tokens) + self.usage.context_tokens = self.context_token_estimate + result = LettaResponse( + messages=response_letta_messages, + stop_reason=self.stop_reason, + usage=self.usage, + logprobs=self.logprobs, + turns=self.turns if self.return_token_ids and self.turns else None, + ) + if run_id: + if self.job_update_metadata is None: + self.job_update_metadata = {} + self.job_update_metadata["result"] = result.model_dump(mode="json") + + await self._request_checkpoint_finish( + request_span=request_span, request_start_timestamp_ns=request_start_timestamp_ns, run_id=run_id + ) + return result + + @trace_method + async def stream( + self, + input_messages: list[MessageCreate], + max_steps: int = DEFAULT_MAX_STEPS, + stream_tokens: bool = False, + run_id: str | None = None, + use_assistant_message: bool = True, # NOTE: not used + include_return_message_types: list[MessageType] | None = None, + request_start_timestamp_ns: int | None = None, + conversation_id: str | None = None, + client_tools: list[ClientToolSchema] | None = None, + client_skills: list[ClientSkillSchema] | None = None, + override_system: str | None = None, + include_compaction_messages: bool = False, + billing_context: BillingContext | None = None, + openai_responses_websocket: bool = False, + ) -> AsyncGenerator[str, None]: + """ + Execute the agent loop in streaming mode, yielding chunks as they become available. + If stream_tokens is True, individual tokens are streamed as they arrive from the LLM, + providing the lowest latency experience, otherwise each complete step (reasoning + + tool call + tool return) is yielded as it completes. + + Args: + input_messages: List of new messages to process + max_steps: Maximum number of agent steps to execute + stream_tokens: Whether to stream back individual tokens. Not all llm + providers offer native token streaming functionality; in these cases, + this api streams back steps rather than individual tokens. + run_id: Optional job/run ID for tracking + use_assistant_message: Whether to use assistant message format + include_return_message_types: Filter for which message types to return + request_start_timestamp_ns: Start time for tracking request duration + conversation_id: Optional conversation ID for conversation-scoped messaging + client_tools: Optional list of client-side tools. When called, execution pauses + for client to provide tool returns. + openai_responses_websocket: If True, use WebSocket transport for OpenAI Responses API. + + Yields: + str: JSON-formatted SSE data chunks for each completed step + """ + self._initialize_state() + self.conversation_id = conversation_id + self.client_tools = client_tools or [] + self.client_skills = client_skills or [] + self.override_system = override_system + request_span = self._request_checkpoint_start(request_start_timestamp_ns=request_start_timestamp_ns) + response_letta_messages = [] + first_chunk = True + + # Apply conversation-specific block overrides if conversation_id is provided + if conversation_id: + self.agent_state = await ConversationManager().apply_isolated_blocks_to_agent_state( + agent_state=self.agent_state, + conversation_id=conversation_id, + actor=self.actor, + ) + + # Check if we should use SGLang native adapter for multi-turn RL training + use_sglang_native = ( + self.agent_state.llm_config.return_token_ids + and self.agent_state.llm_config.handle + and self.agent_state.llm_config.handle.startswith("sglang/") + ) + self.return_token_ids = use_sglang_native + + if stream_tokens: + llm_adapter = SimpleLLMStreamAdapter( + llm_client=self.llm_client, + llm_config=self.agent_state.llm_config, + call_type=LLMCallType.agent_step, + agent_id=self.agent_state.id, + agent_tags=self.agent_state.tags, + run_id=run_id, + org_id=self.actor.organization_id, + user_id=self.actor.id, + billing_context=billing_context, + use_openai_responses_websocket=openai_responses_websocket, + ) + elif use_sglang_native: + # Use SGLang native adapter for multi-turn RL training + llm_adapter = SGLangNativeAdapter( + llm_client=self.llm_client, + llm_config=self.agent_state.llm_config, + model_settings=self.agent_state.model_settings, + call_type=LLMCallType.agent_step, + agent_id=self.agent_state.id, + agent_tags=self.agent_state.tags, + run_id=run_id, + org_id=self.actor.organization_id, + user_id=self.actor.id, + billing_context=billing_context, + ) + # Reset turns tracking for this step + self.turns = [] + else: + llm_adapter = SimpleLLMRequestAdapter( + llm_client=self.llm_client, + llm_config=self.agent_state.llm_config, + call_type=LLMCallType.agent_step, + agent_id=self.agent_state.id, + agent_tags=self.agent_state.tags, + run_id=run_id, + org_id=self.actor.organization_id, + user_id=self.actor.id, + billing_context=billing_context, + ) + + try: + # Prepare in-context messages (conversation mode if conversation_id provided) + in_context_messages, input_messages_to_persist = await _prepare_in_context_messages_no_persist_async( + input_messages, + self.agent_state, + self.message_manager, + self.actor, + run_id, + conversation_id=conversation_id, + ) + follow_up_messages = [] + if len(input_messages_to_persist) > 1 and input_messages_to_persist[0].role == "approval": + follow_up_messages = input_messages_to_persist[1:] + input_messages_to_persist = [input_messages_to_persist[0]] + + self.in_context_messages = in_context_messages + credit_task = None + for i in range(max_steps): + if i == 1 and follow_up_messages: + input_messages_to_persist = follow_up_messages + follow_up_messages = [] + + # Await credit check from previous iteration before running next step + if credit_task is not None: + if not await credit_task: + self.should_continue = False + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.insufficient_credits) + break + credit_task = None + + response = self._step( + # we append input_messages_to_persist since they aren't checkpointed as in-context until the end of the step (may be rolled back) + messages=list(self.in_context_messages + input_messages_to_persist), + input_messages_to_persist=input_messages_to_persist, + llm_adapter=llm_adapter, + run_id=run_id, + # use_assistant_message=use_assistant_message, + include_return_message_types=include_return_message_types, + request_start_timestamp_ns=request_start_timestamp_ns, + include_compaction_messages=include_compaction_messages, + billing_context=billing_context, + ) + input_messages_to_persist = [] # clear after first step + async for chunk in response: + response_letta_messages.append(chunk) + if first_chunk: + request_span = self._request_checkpoint_ttft(request_span, request_start_timestamp_ns) + + # Log chunks with missing id or otid for debugging. + # Compaction EventMessage is intentionally metadata-only and may omit otid. + is_compaction_event = isinstance(chunk, EventMessage) and chunk.event_type == "compaction" + if isinstance(chunk, LettaMessage) and (not chunk.id or not chunk.otid) and not is_compaction_event: + self.logger.warning( + "Streaming chunk missing id or otid: message_type=%s id=%s otid=%s step_id=%s", + chunk.message_type, + chunk.id, + chunk.otid, + chunk.step_id, + ) + + yield f"data: {chunk.model_dump_json()}\n\n" + first_chunk = False + + # Check if step was cancelled - break out of the step loop + if not self.should_continue and self.stop_reason.stop_reason == StopReasonType.cancelled.value: + break + + # refresh in-context messages (TODO: remove?) + # in_context_messages = await self._refresh_messages(in_context_messages) + + if not self.should_continue: + break + + # Fire credit check to run in parallel with loop overhead / next step setup + credit_task = safe_create_task_with_return(self._check_credits()) + + if i == max_steps - 1 and self.stop_reason is None: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.max_steps.value) + + ## Rebuild context window after stepping (safety net) + # if not self.agent_state.message_buffer_autoclear: + # if self.context_token_estimate is not None: + # await self.summarize_conversation_history( + # in_context_messages=in_context_messages, + # new_letta_messages=self.response_messages, + # total_tokens=self.context_token_estimate, + # force=False, + # ) + # else: + # self.logger.warning( + # "Post-loop summarization skipped: last_step_usage is None. " + # "No step completed successfully or usage stats were not updated." + # ) + + if self.stop_reason is None: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + + except Exception as e: + # Use repr() if str() is empty (happens with Exception() with no args) + error_detail = str(e) or repr(e) + self.logger.warning(f"Error during agent stream: {error_detail}", exc_info=True) + + # Set stop_reason if not already set + if self.stop_reason is None: + # Classify error type + if isinstance(e, SystemPromptTokenExceededError): + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.context_window_overflow_in_system_prompt.value) + elif isinstance(e, LLMError): + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.llm_api_error.value) + else: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + + if first_chunk: + # Raise if no chunks sent yet (response not started, can return error status code) + await llm_adapter.aclose() + raise + else: + yield f"data: {self.stop_reason.model_dump_json()}\n\n" + + # Mid-stream error: yield error event to client in SSE format + user_visible_error_message = "An error occurred during agent execution." + error_type = "internal_error" + if isinstance(e, SystemPromptTokenExceededError): + error_type = StopReasonType.context_window_overflow_in_system_prompt.value + user_visible_error_message = ( + "Compaction failed because the system prompt is too large for this model's context window. " + "Reduce system instructions, memory blocks, or tools, or use a model with a larger context window." + ) + + error_message = LettaErrorMessage( + run_id=run_id, + error_type=error_type, + message=user_visible_error_message, + detail=error_detail, + ) + yield f"event: error\ndata: {error_message.model_dump_json()}\n\n" + + # Return immediately - don't fall through to finish chunks + # This prevents sending end_turn finish chunks after an error + await llm_adapter.aclose() + return + + # Cleanup and finalize (only runs if no exception occurred) + try: + # Set context_tokens to expose actual context window usage (vs accumulated prompt_tokens) + self.usage.context_tokens = self.context_token_estimate + + if run_id: + # Filter out LettaStopReason from messages (only valid in LettaStreamingResponse, not LettaResponse) + filtered_messages = [m for m in response_letta_messages if not isinstance(m, LettaStopReason)] + result = LettaResponse( + messages=filtered_messages, + stop_reason=self.stop_reason, + usage=self.usage, + logprobs=self.logprobs, + turns=self.turns if self.return_token_ids and self.turns else None, + ) + if self.job_update_metadata is None: + self.job_update_metadata = {} + self.job_update_metadata["result"] = result.model_dump(mode="json") + + await self._request_checkpoint_finish( + request_span=request_span, request_start_timestamp_ns=request_start_timestamp_ns, run_id=run_id + ) + for finish_chunk in self.get_finish_chunks_for_stream(self.usage, self.stop_reason): + yield f"data: {finish_chunk}\n\n" + except Exception as cleanup_error: + # Error during cleanup/finalization - ensure we still send a terminal event + self.logger.error(f"Error during stream cleanup: {cleanup_error}", exc_info=True) + + # Set stop_reason if not already set + if self.stop_reason is None: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + + yield f"data: {self.stop_reason.model_dump_json()}\n\n" + + # Send error event + error_message = LettaErrorMessage( + run_id=run_id, + error_type="cleanup_error", + message="An error occurred during stream finalization.", + detail=str(cleanup_error), + ) + yield f"event: error\ndata: {error_message.model_dump_json()}\n\n" + # Note: we don't send finish chunks here since we already errored + finally: + # Ensure adapter resources (e.g. WebSocket connections) are cleaned up + await llm_adapter.aclose() + + async def _check_for_system_prompt_overflow(self, system_message): + """ + Since the system prompt cannot be compacted, we need to check to see if it is the cause of the context overflow + """ + system_prompt_token_estimate = await count_tokens( + actor=self.actor, + llm_config=self.agent_state.llm_config, + messages=[system_message], + ) + if system_prompt_token_estimate is not None and system_prompt_token_estimate >= self.agent_state.llm_config.context_window: + self.should_continue = False + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.context_window_overflow_in_system_prompt.value) + raise SystemPromptTokenExceededError( + system_prompt_token_estimate=system_prompt_token_estimate, + context_window=self.agent_state.llm_config.context_window, + ) + + async def _checkpoint_messages(self, run_id: str, step_id: str, new_messages: list[Message], in_context_messages: list[Message]): + """ + Checkpoint the current message state - run this only when the current messages are 'safe' - meaning the step has completed successfully. + + This handles: + - Persisting the new messages into the `messages` table + - Updating the in-memory trackers for in-context messages (`self.in_context_messages`) and agent state (`self.agent_state.message_ids`) + - Updating the DB with the current in-context messages (`self.agent_state.message_ids`) OR conversation_messages table + + Args: + run_id: The run ID to associate with the messages + step_id: The step ID to associate with the messages + new_messages: The new messages to persist + in_context_messages: The current in-context messages + """ + # make sure all the new messages have the correct run_id, step_id, and conversation_id + for message in new_messages: + message.step_id = step_id + message.run_id = run_id + message.conversation_id = self.conversation_id + + # persist the new message objects - ONLY place where messages are persisted + await self.message_manager.create_many_messages_async( + new_messages, + actor=self.actor, + run_id=run_id, + project_id=self.agent_state.project_id, + template_id=self.agent_state.template_id, + ) + + if self.conversation_id: + # Conversation mode: update conversation_messages table + # Add new messages to conversation tracking + new_message_ids = [m.id for m in new_messages] + if new_message_ids: + await ConversationManager().add_messages_to_conversation( + conversation_id=self.conversation_id, + agent_id=self.agent_state.id, + message_ids=new_message_ids, + actor=self.actor, + ) + + # Update which messages are in context + # Note: update_in_context_messages also updates positions to preserve order + await ConversationManager().update_in_context_messages( + conversation_id=self.conversation_id, + in_context_message_ids=[m.id for m in in_context_messages], + actor=self.actor, + ) + else: + # Default mode: update agent.message_ids + await self.agent_manager.update_message_ids_async( + agent_id=self.agent_state.id, + message_ids=[m.id for m in in_context_messages], + actor=self.actor, + ) + self.agent_state.message_ids = [m.id for m in in_context_messages] # update in-memory state + + self.in_context_messages = in_context_messages # update in-memory state + + def _create_compaction_event_message( + self, + step_id: str | None, + run_id: str | None, + trigger: str, + ) -> EventMessage: + """ + Create an EventMessage to notify the client that compaction is starting. + + Args: + step_id: The current step ID + run_id: The current run ID + trigger: The trigger that caused compaction (e.g., "context_window_exceeded", "post_step_context_check") + + Returns: + EventMessage to yield before compaction starts + """ + return EventMessage( + id=str(uuid.uuid4()), + date=get_utc_time(), + event_type="compaction", + event_data={ + "trigger": trigger, + "context_token_estimate": self.context_token_estimate, + "context_window": self.agent_state.llm_config.context_window, + }, + run_id=run_id, + step_id=step_id, + ) + + def _create_summary_result_message( + self, + summary_message: Message, + summary_text: str, + step_id: str | None, + run_id: str | None, + include_compaction_messages: bool, + ) -> list[LettaMessage]: + """ + Create the summary message to yield to the client after compaction completes. + + Args: + summary_message: The persisted summary Message object + summary_text: The raw summary text (unpacked) + step_id: The current step ID + run_id: The current run ID + include_compaction_messages: If True, return SummaryMessage; if False, return UserMessage + + Returns: + List of LettaMessage objects to yield to the client + """ + if include_compaction_messages: + # Extract compaction_stats from the packed message content if available + compaction_stats = extract_compaction_stats_from_message(summary_message) + + # New behavior: structured SummaryMessage + return [ + SummaryMessage( + id=summary_message.id, + date=summary_message.created_at, + summary=summary_text, + otid=Message.generate_otid_from_id(summary_message.id, 0), + step_id=step_id, + run_id=run_id, + compaction_stats=compaction_stats, + ), + ] + else: + # Old behavior: UserMessage with packed JSON + messages = list(Message.to_letta_messages(summary_message)) + # Set otid on returned messages (summary Message doesn't have otid set at creation) + for i, msg in enumerate(messages): + if not msg.otid: + msg.otid = Message.generate_otid_from_id(summary_message.id, i) + return messages + + @trace_method + async def _step( + self, + messages: list[Message], # current in-context messages + llm_adapter: LettaLLMAdapter, + input_messages_to_persist: list[Message] | None = None, + run_id: str | None = None, + # use_assistant_message: bool = True, + include_return_message_types: list[MessageType] | None = None, + request_start_timestamp_ns: int | None = None, + remaining_turns: int = -1, + dry_run: bool = False, + enforce_run_id_set: bool = True, + include_compaction_messages: bool = False, + billing_context: Optional["BillingContext"] = None, + ) -> AsyncGenerator[LettaMessage | dict, None]: + """ + Execute a single agent step (one LLM call and tool execution). + + This is the core execution method that all public methods (step, stream_steps, + stream_tokens) funnel through. It handles the complete flow of making an LLM + request, processing the response, executing tools, and persisting messages. + + Args: + messages: Current in-context messages + llm_adapter: Adapter for LLM interaction (blocking or streaming) + input_messages_to_persist: New messages to persist after execution + run_id: Optional job/run ID for tracking + include_return_message_types: Filter for which message types to yield + request_start_timestamp_ns: Start time for tracking request duration + remaining_turns: Number of turns remaining (for max_steps enforcement) + dry_run: If true, only build and return the request without executing + + Yields: + LettaMessage or dict: Chunks for streaming mode, or request data for dry_run + """ + if enforce_run_id_set and run_id is None: + raise AssertionError("run_id is required when enforce_run_id_set is True") + + input_messages_to_persist = input_messages_to_persist or [] + + if self.context_token_estimate is None: + self.logger.warning("Context token estimate is not set") + + compaction_trigger_threshold = get_compaction_trigger_threshold(self.agent_state.llm_config) + + step_progression = StepProgression.START + caught_exception = None + # TODO(@caren): clean this up + tool_calls, content, agent_step_span, _first_chunk, step_id, logged_step, _step_start_ns, step_metrics = ( + None, + None, + None, + None, + None, + None, + None, + None, + ) + try: + self.last_function_response = _load_last_function_response(messages) + valid_tools = await self._get_valid_tools() + require_tool_call = self.tool_rules_solver.should_force_tool_call() + + if self._require_tool_call != require_tool_call: + if require_tool_call: + self.logger.info("switching to constrained mode (forcing tool call)") + else: + self.logger.info("switching to unconstrained mode (allowing non-tool responses)") + self._require_tool_call = require_tool_call + + # Refresh messages at the start of each step to scrub inner thoughts. + # NOTE: We skip system prompt refresh during normal steps to preserve prefix caching. + # The system prompt is only rebuilt after compaction or message reset. + try: + messages = await self._refresh_messages(messages) + except Exception as e: + self.logger.warning(f"Failed to refresh messages at step start: {e}") + + approval_request, approval_response = _maybe_get_approval_messages(messages) + tool_call_denials, tool_returns = [], [] + if approval_request and approval_response: + # case of handling approval responses + content = approval_request.content + + # Get tool calls that are pending + backfill_tool_call_id = approval_request.tool_calls[0].id # legacy case + if approval_response.approvals: + approved_tool_call_ids = { + backfill_tool_call_id if a.tool_call_id.startswith("message-") else a.tool_call_id + for a in approval_response.approvals + if isinstance(a, ApprovalReturn) and a.approve + } + else: + approved_tool_call_ids = {} + tool_calls = [tool_call for tool_call in approval_request.tool_calls if tool_call.id in approved_tool_call_ids] + pending_tool_call_message = _maybe_get_pending_tool_call_message(messages) + if pending_tool_call_message: + tool_calls.extend(pending_tool_call_message.tool_calls) + + # Get tool calls that were denied + if approval_response.approvals: + denies = {d.tool_call_id: d for d in approval_response.approvals if isinstance(d, ApprovalReturn) and not d.approve} + else: + denies = {} + tool_call_denials = [ + ToolCallDenial(**t.model_dump(), reason=denies.get(t.id).reason) for t in approval_request.tool_calls if t.id in denies + ] + + # Get tool calls that were executed client side + if approval_response.approvals: + tool_returns = [r for r in approval_response.approvals if isinstance(r, ToolReturn)] + + # Validate that the approval response contains meaningful data + # If all three lists are empty, this is a malformed approval response + if not tool_calls and not tool_call_denials and not tool_returns: + self.logger.error( + f"Invalid approval response: approval_response.approvals is {approval_response.approvals} " + f"but no tool calls, denials, or returns were extracted. " + f"This likely indicates a corrupted or malformed approval payload." + ) + self.should_continue = False + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.invalid_tool_call.value) + return + + step_id = approval_request.step_id + if step_id is None: + # Old approval messages may not have step_id set - generate a new one + self.logger.warning(f"Approval request message {approval_request.id} has no step_id, generating new step_id") + step_id = generate_step_id() + step_progression, logged_step, step_metrics, agent_step_span = await self._step_checkpoint_start( + step_id=step_id, run_id=run_id + ) + else: + step_metrics = await self.step_manager.get_step_metrics_async(step_id=step_id, actor=self.actor) + else: + # Check for job cancellation at the start of each step + if run_id and await self._check_run_cancellation(run_id): + self.should_continue = False + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.cancelled.value) + self.logger.info(f"Agent execution cancelled for run {run_id}") + return + + step_id = generate_step_id() + step_progression, logged_step, step_metrics, agent_step_span = await self._step_checkpoint_start( + step_id=step_id, run_id=run_id + ) + + # Auto mode: resolve handle to actual model config + auto_mode_handle = self.agent_state.llm_config.handle + is_auto_mode = auto_mode_handle in AUTO_MODE_HANDLES + is_primary = False + primary_handle = "" + + if is_auto_mode: + resolved_llm_config = None + try: + routing_client = await get_llm_routing_client() + active_llm_config, is_primary, primary_handle = await routing_client.resolve_auto_mode_config( + stored_llm_config=self.agent_state.llm_config, + actor=self.actor, + ) + resolved_llm_config = active_llm_config + if not is_primary: + self.logger.info(f"[LLM ROUTER]: primary {primary_handle} rerouted, falling back to {active_llm_config.handle}") + # Content-based rerouting (e.g. images → vision-capable model) + active_llm_config = routing_client.apply_reroute_rules( + resolved_config=active_llm_config, + messages=messages, + stored_llm_config=self.agent_state.llm_config, + agent_state=self.agent_state, + ) + resolved_llm_config = active_llm_config + active_llm_client = LLMClient.create( + provider_type=active_llm_config.model_endpoint_type, + put_inner_thoughts_first=True, + actor=self.actor, + ) + # Update the adapter to use the resolved client and config + llm_adapter.llm_client = active_llm_client + llm_adapter.llm_config = active_llm_config + finally: + # Update persisted step with resolved model info so billing can + # identify the actual model and charge at the correct rate, + # even if resolution fails partway through. + if resolved_llm_config is not None: + await self.step_manager.update_step_resolved_model_async( + actor=self.actor, + step_id=step_id, + provider_name=resolved_llm_config.model_endpoint_type, + provider_category=resolved_llm_config.provider_category or "base", + model=resolved_llm_config.model, + model_endpoint=resolved_llm_config.model_endpoint, + ) + else: + active_llm_config = self.agent_state.llm_config + active_llm_client = self.llm_client + + force_tool_call = valid_tools[0]["name"] if len(valid_tools) == 1 and self._require_tool_call else None + for llm_request_attempt in range(summarizer_settings.max_summarizer_retries + 1): + try: + request_system_prompt = self.generate_request_system_prompt( + client_skills=self.client_skills, + current_system_message=messages[0], + ) + request_data = active_llm_client.build_request_data( + agent_type=self.agent_state.agent_type, + messages=messages, + llm_config=active_llm_config, + tools=valid_tools, + force_tool_call=force_tool_call, + requires_subsequent_tool_call=self._require_tool_call, + tool_return_truncation_chars=self._compute_tool_return_truncation_chars(), + system=request_system_prompt, + ) + # TODO: Extend to more providers, and also approval tool rules + # TODO: this entire code block should be inside of the clients + # Enable parallel tool use when no tool rules are attached + try: + no_tool_rules = ( + not self.agent_state.tool_rules + or len([t for t in self.agent_state.tool_rules if t.type != "requires_approval"]) == 0 + ) + + # Anthropic/Bedrock/MiniMax parallel tool use (MiniMax uses Anthropic-compatible API) + if active_llm_config.model_endpoint_type in ["anthropic", "bedrock", "minimax"]: + if ( + isinstance(request_data.get("tool_choice"), dict) + and "disable_parallel_tool_use" in request_data["tool_choice"] + ): + # Gate parallel tool use on both: no tool rules and toggled on + if no_tool_rules and active_llm_config.parallel_tool_calls: + request_data["tool_choice"]["disable_parallel_tool_use"] = False + else: + # Explicitly disable when tool rules present or llm_config toggled off + request_data["tool_choice"]["disable_parallel_tool_use"] = True + + # OpenAI parallel tool use + elif active_llm_config.model_endpoint_type == "openai": + # For OpenAI, we control parallel tool calling via parallel_tool_calls field + # Only allow parallel tool calls when no tool rules and enabled in config + if "parallel_tool_calls" in request_data: + if no_tool_rules and active_llm_config.parallel_tool_calls: + request_data["parallel_tool_calls"] = True + else: + request_data["parallel_tool_calls"] = False + + # Gemini (Google AI/Vertex) parallel tool use + elif active_llm_config.model_endpoint_type in ["google_ai", "google_vertex"]: + # Gemini supports parallel tool calling natively through multiple parts in the response + # We just need to ensure the config flag is set for tracking purposes + # The actual handling happens in GoogleVertexClient.convert_response_to_chat_completion + pass # No specific request_data field needed for Gemini + except Exception: + # if this fails, we simply don't enable parallel tool use + pass + if dry_run: + yield request_data + return + + step_progression, step_metrics = self._step_checkpoint_llm_request_start(step_metrics, agent_step_span) + invocation = llm_adapter.invoke_llm( + request_data=request_data, + messages=messages, + tools=valid_tools, + use_assistant_message=False, # NOTE: set to false + requires_approval_tools=self.tool_rules_solver.get_requires_approval_tools( + set([t["name"] for t in valid_tools]) + ) + + [ct.name for ct in self.client_tools], + step_id=step_id, + actor=self.actor, + ) + async for chunk in invocation: + if llm_adapter.supports_token_streaming(): + if include_return_message_types is None or chunk.message_type in include_return_message_types: + yield chunk + # Report success to circuit breaker (only for models with fallback routes) + routing_client = await get_llm_routing_client() + if routing_client.get_fallback_handle(active_llm_config.handle): + await routing_client.record_success(active_llm_config.handle) + # If you've reached this point without an error, break out of retry loop + break + except ValueError as e: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.invalid_llm_response.value) + raise e + except LLMEmptyResponseError as e: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.invalid_llm_response.value) + raise e + except (LLMRateLimitError, LLMServerError, LLMProviderOverloaded) as e: + # Check if there's a fallback route for the current model + routing_client = await get_llm_routing_client() + current_handle = active_llm_config.handle + fallback_handle = routing_client.get_fallback_handle(current_handle) + + if fallback_handle: + await routing_client.record_failure(current_handle) + + fallback_config = await routing_client.get_fallback_config_for_handle( + fallback_handle=fallback_handle, + stored_llm_config=self.agent_state.llm_config, + actor=self.actor, + ) + self.logger.warning( + f"[LLM ROUTER]: {current_handle} failed ({type(e).__name__}), falling back to {fallback_config.handle}" + ) + + # Switch to fallback for this attempt and any subsequent retries (e.g. compaction) + active_llm_config = fallback_config + active_llm_client = LLMClient.create( + provider_type=fallback_config.model_endpoint_type, + put_inner_thoughts_first=True, + actor=self.actor, + ) + llm_adapter.llm_client = active_llm_client + llm_adapter.llm_config = active_llm_config + is_primary = False + continue + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.llm_api_error.value) + raise e + except LLMError as e: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.llm_api_error.value) + raise e + except Exception as e: + if isinstance(e, ContextWindowExceededError) and llm_request_attempt < summarizer_settings.max_summarizer_retries: + # Retry case + self.logger.info( + f"Context window exceeded (error {e}), trying to compact messages attempt {llm_request_attempt + 1} of {summarizer_settings.max_summarizer_retries + 1}" + ) + try: + # Capture pre-compaction state for metadata + context_tokens_before = self.context_token_estimate + messages_count_before = len(messages) + + # Yield event notification before compaction starts + if include_compaction_messages: + yield self._create_compaction_event_message( + step_id=step_id, + run_id=run_id, + trigger="context_window_exceeded", + ) + + # Ensure system prompt is recompiled before summarization so compaction + # operates on the latest system+memory state (including recent repairs). + # NOTE: we no longer refresh the system prompt before compaction so we can leverage cache for self mode + # messages = await self._refresh_messages(messages, force_system_prompt_refresh=True) + + summary_message, messages, summary_text = await self.compact( + messages, + trigger_threshold=compaction_trigger_threshold, + run_id=run_id, + step_id=step_id, + use_summary_role=include_compaction_messages, + trigger="context_window_exceeded", + context_tokens_before=context_tokens_before, + messages_count_before=messages_count_before, + billing_context=billing_context, + ) + + # Recompile the persisted system prompt after compaction so subsequent + # turns load the repaired system+memory state from message_ids[0]. + await self.agent_manager.rebuild_system_prompt_async( + agent_id=self.agent_state.id, + actor=self.actor, + force=True, + update_timestamp=True, + ) + # Force system prompt rebuild after compaction to update memory blocks and timestamps + messages = await self._refresh_messages(messages, force_system_prompt_refresh=True) + self.logger.info("Summarization succeeded, continuing to retry LLM request") + + # Persist the summary message + self.response_messages.append(summary_message) + await self._checkpoint_messages( + run_id=run_id, + step_id=step_id, + new_messages=[summary_message], + in_context_messages=messages, + ) + + # Yield summary result message to client + for msg in self._create_summary_result_message( + summary_message=summary_message, + summary_text=summary_text, + step_id=step_id, + run_id=run_id, + include_compaction_messages=include_compaction_messages, + ): + yield msg + + continue + except SystemPromptTokenExceededError: + self.should_continue = False + self.stop_reason = LettaStopReason( + stop_reason=StopReasonType.context_window_overflow_in_system_prompt.value + ) + raise + except Exception as e: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + self.logger.error(f"Unknown error occured for summarization run {run_id}: {e}") + raise e + + else: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + self.logger.error(f"Unknown error occured for run {run_id}: {e}") + raise e + + step_progression, step_metrics = self._step_checkpoint_llm_request_finish( + step_metrics, agent_step_span, llm_adapter.llm_request_finish_timestamp_ns + ) + # update metrics + self._update_global_usage_stats(llm_adapter.usage) + self.context_token_estimate = llm_adapter.usage.total_tokens + self.logger.info(f"Context token estimate after LLM request: {self.context_token_estimate}") + + # Extract logprobs if present (for RL training) + if llm_adapter.logprobs is not None: + self.logprobs = llm_adapter.logprobs + + # Track turn data for multi-turn RL training (SGLang native mode) + if self.return_token_ids and hasattr(llm_adapter, "output_ids") and llm_adapter.output_ids: + self.turns.append( + TurnTokenData( + role="assistant", + output_ids=llm_adapter.output_ids, + output_token_logprobs=llm_adapter.output_token_logprobs, + content=llm_adapter.chat_completions_response.choices[0].message.content + if llm_adapter.chat_completions_response + else None, + ) + ) + + # Handle the AI response with the extracted data (supports multiple tool calls) + # Gather tool calls - check for multi-call API first, then fall back to single + if hasattr(llm_adapter, "tool_calls") and llm_adapter.tool_calls: + tool_calls = llm_adapter.tool_calls + elif llm_adapter.tool_call is not None: + tool_calls = [llm_adapter.tool_call] + else: + tool_calls = [] + + # Enforce parallel_tool_calls=false by truncating to first tool call + # Some providers (e.g. Gemini) don't respect this setting via API, so we enforce it client-side + if len(tool_calls) > 1 and not active_llm_config.parallel_tool_calls: + self.logger.warning( + f"LLM returned {len(tool_calls)} tool calls but parallel_tool_calls=false. " + f"Truncating to first tool call: {tool_calls[0].function.name}" + ) + tool_calls = [tool_calls[0]] + + # get the new generated `Message` objects from handling the LLM response + new_messages, self.should_continue, self.stop_reason = await self._handle_ai_response( + tool_calls=tool_calls, + valid_tool_names=[tool["name"] for tool in valid_tools], + tool_rules_solver=self.tool_rules_solver, + usage=UsageStatistics( + completion_tokens=self.usage.completion_tokens, + prompt_tokens=self.usage.prompt_tokens, + total_tokens=self.usage.total_tokens, + ), + content=content or llm_adapter.content, + pre_computed_assistant_message_id=llm_adapter.message_id, + step_id=step_id, + initial_messages=[], # input_messages_to_persist, # TODO: deprecate - super confusing + agent_step_span=agent_step_span, + is_final_step=(remaining_turns == 0), + run_id=run_id, + step_metrics=step_metrics, + is_approval_response=approval_response is not None, + tool_call_denials=tool_call_denials, + tool_returns=tool_returns, + finish_reason=llm_adapter.finish_reason, + ) + + # extend trackers with new messages + self.response_messages.extend(new_messages) + messages.extend(new_messages) + + # Track tool return turns for multi-turn RL training + if self.return_token_ids: + for msg in new_messages: + if msg.role == "tool": + # Get tool return content + tool_content = None + tool_name = None + if hasattr(msg, "tool_returns") and msg.tool_returns: + # Aggregate all tool returns into content (func_response is the actual content) + parts = [] + for tr in msg.tool_returns: + if hasattr(tr, "func_response") and tr.func_response: + if isinstance(tr.func_response, str): + parts.append(tr.func_response) + else: + parts.append(str(tr.func_response)) + tool_content = "\n".join(parts) + elif hasattr(msg, "content") and msg.content: + tool_content = msg.content if isinstance(msg.content, str) else str(msg.content) + if hasattr(msg, "name"): + tool_name = msg.name + if tool_content: + self.turns.append( + TurnTokenData( + role="tool", + content=tool_content, + tool_name=tool_name, + ) + ) + + # step(...) has successfully completed! now we can persist messages and update the in-context messages + save metrics + # persistence needs to happen before streaming to minimize chances of agent getting into an inconsistent state + step_progression, step_metrics = await self._step_checkpoint_finish(step_metrics, agent_step_span, logged_step) + await self._checkpoint_messages( + run_id=run_id, + step_id=step_id, + new_messages=input_messages_to_persist + new_messages, + in_context_messages=messages, # update the in-context messages + ) + + # yield back generated messages + if llm_adapter.supports_token_streaming(): + if tool_calls: + # Stream each tool return if tools were executed + response_tool_returns = [msg for msg in new_messages if msg.role == "tool"] + for tr in response_tool_returns: + # Skip streaming for aggregated parallel tool returns (no per-call tool_call_id) + if tr.tool_call_id is None and tr.tool_returns: + continue + tool_return_letta = tr.to_letta_messages()[0] + if include_return_message_types is None or tool_return_letta.message_type in include_return_message_types: + yield tool_return_letta + else: + # TODO: modify this use step_response_messages + filter_user_messages = [m for m in new_messages if m.role != "user"] + letta_messages = Message.to_letta_messages_from_list( + filter_user_messages, + use_assistant_message=False, # NOTE: set to false + reverse=False, + # text_is_assistant_message=(self.agent_state.agent_type == AgentType.react_agent), + text_is_assistant_message=True, + ) + for message in letta_messages: + if include_return_message_types is None or message.message_type in include_return_message_types: + yield message + + # check compaction + if self.context_token_estimate is not None and self.context_token_estimate > compaction_trigger_threshold: + self.logger.info( + "Compaction threshold exceeded " + f"(current: {self.context_token_estimate}, threshold: {compaction_trigger_threshold}, " + f"context_window: {self.agent_state.llm_config.context_window}), trying to compact messages" + ) + + # Capture pre-compaction state for metadata + context_tokens_before = self.context_token_estimate + messages_count_before = len(messages) + + # Yield event notification before compaction starts + if include_compaction_messages: + yield self._create_compaction_event_message( + step_id=step_id, + run_id=run_id, + trigger="post_step_context_check", + ) + + try: + # Ensure system prompt is recompiled before summarization so compaction + # operates on the latest system+memory state (including recent repairs). + # NOTE: we no longer refresh the system prompt before compaction so we can leverage cache for self mode + # messages = await self._refresh_messages(messages, force_system_prompt_refresh=True) + + summary_message, messages, summary_text = await self.compact( + messages, + trigger_threshold=compaction_trigger_threshold, + run_id=run_id, + step_id=step_id, + use_summary_role=include_compaction_messages, + trigger="post_step_context_check", + context_tokens_before=context_tokens_before, + messages_count_before=messages_count_before, + billing_context=billing_context, + ) + + # Recompile the persisted system prompt after compaction so subsequent + # turns load the repaired system+memory state from message_ids[0]. + await self.agent_manager.rebuild_system_prompt_async( + agent_id=self.agent_state.id, + actor=self.actor, + force=True, + update_timestamp=True, + ) + # Force system prompt rebuild after compaction to update memory blocks and timestamps + messages = await self._refresh_messages(messages, force_system_prompt_refresh=True) + # TODO: persist + return the summary message + # TODO: convert this to a SummaryMessage + self.response_messages.append(summary_message) + + # Yield summary result message to client + for msg in self._create_summary_result_message( + summary_message=summary_message, + summary_text=summary_text, + step_id=step_id, + run_id=run_id, + include_compaction_messages=include_compaction_messages, + ): + yield msg + + await self._checkpoint_messages( + run_id=run_id, + step_id=step_id, + new_messages=[summary_message], + in_context_messages=messages, + ) + except SystemPromptTokenExceededError: + self.should_continue = False + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.context_window_overflow_in_system_prompt.value) + raise + + except Exception as e: + caught_exception = e + # NOTE: message persistence does not happen in the case of an exception (rollback to previous state) + # Use repr() if str() is empty (happens with Exception() with no args) + error_detail = str(e) or repr(e) + self.logger.warning(f"Error during step processing: {error_detail}") + self.job_update_metadata = {"error": error_detail} + + # Stop the agent loop on any exception to prevent wasteful retry loops + # (e.g., if post-step compaction fails, we don't want to keep retrying) + self.should_continue = False + self.logger.warning( + f"Agent loop stopped due to exception (step_progression={step_progression.name}, " + f"exception_type={type(e).__name__}): {error_detail}" + ) + + # This indicates we failed after we decided to stop stepping, which indicates a bug with our flow. + if not self.stop_reason: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + elif self.stop_reason.stop_reason in (StopReasonType.end_turn, StopReasonType.max_steps, StopReasonType.tool_rule): + self.logger.warning("Error occurred during step processing, with valid stop reason: %s", self.stop_reason.stop_reason) + elif self.stop_reason.stop_reason not in ( + StopReasonType.no_tool_call, + StopReasonType.invalid_tool_call, + StopReasonType.invalid_llm_response, + StopReasonType.llm_api_error, + StopReasonType.context_window_overflow_in_system_prompt, + ): + self.logger.warning("Error occurred during step processing, with unexpected stop reason: %s", self.stop_reason.stop_reason) + raise e + finally: + # always make sure we update the step/run metadata + self.logger.debug("Running cleanup for agent loop run: %s", run_id) + self.logger.info("Running final update. Step Progression: %s", step_progression) + try: + if step_progression == StepProgression.FINISHED: + if not self.should_continue: + if self.stop_reason is None: + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + if logged_step and step_id: + await self.step_manager.update_step_stop_reason(self.actor, step_id, self.stop_reason.stop_reason) + if not self.stop_reason or self.stop_reason.stop_reason != StopReasonType.context_window_overflow_in_system_prompt: + # only return if the stop reason is not context window overflow in system prompt + return + if step_progression < StepProgression.STEP_LOGGED: + # Error occurred before step was fully logged + import traceback + + if logged_step: + await self.step_manager.update_step_error_async( + actor=self.actor, + step_id=step_id, # Use original step_id for telemetry + error_type=type(caught_exception).__name__ if caught_exception is not None else "Unknown", + error_message=str(caught_exception) if caught_exception is not None else "Unknown error", + error_traceback=traceback.format_exc(), + stop_reason=self.stop_reason, + ) + elif step_progression <= StepProgression.LOGGED_TRACE: + if self.stop_reason is None: + self.logger.warning("Error in step after logging step") + self.stop_reason = LettaStopReason(stop_reason=StopReasonType.error.value) + if logged_step: + await self.step_manager.update_step_stop_reason(self.actor, step_id, self.stop_reason.stop_reason) + else: + self.logger.warning("Invalid StepProgression value") + + # Do tracking for failure cases. Can consolidate with success conditions later. + if settings.track_stop_reason: + await self._log_request(request_start_timestamp_ns, None, self.job_update_metadata, is_error=True, run_id=run_id) + + # Record partial step metrics on failure (capture whatever timing data we have) + if logged_step and step_metrics and step_progression < StepProgression.FINISHED: + # Calculate total step time up to the failure point + step_metrics.step_ns = get_utc_timestamp_ns() - step_metrics.step_start_ns + + await self._record_step_metrics( + step_id=step_id, + step_metrics=step_metrics, + run_id=run_id, + ) + except Exception as e: + self.logger.warning(f"Error during post-completion step tracking: {e}") + + @trace_method + async def _handle_ai_response( + self, + valid_tool_names: list[str], + tool_rules_solver: ToolRulesSolver, + usage: UsageStatistics, + content: list[TextContent | ReasoningContent | RedactedReasoningContent | OmittedReasoningContent] | None = None, + pre_computed_assistant_message_id: str | None = None, + step_id: str | None = None, + initial_messages: list[Message] | None = None, + agent_step_span: Span | None = None, + is_final_step: bool | None = None, + run_id: str | None = None, + step_metrics: StepMetrics = None, + is_approval_response: bool | None = None, + tool_calls: list[ToolCall] = [], + tool_call_denials: list[ToolCallDenial] = [], + tool_returns: list[ToolReturn] = [], + finish_reason: str | None = None, + ) -> tuple[list[Message], bool, LettaStopReason | None]: + """ + Handle the final AI response once streaming completes, execute / validate tool calls, + decide whether we should keep stepping, and persist state. + + Unified approach: treats single and multi-tool calls uniformly to reduce code duplication. + """ + + # 1. Handle no-tool cases (content-only or no-op) + if not tool_calls and not tool_call_denials and not tool_returns: + # Case 1a: No tool call, no content (LLM no-op) + if content is None or len(content) == 0: + # Check if there are required-before-exit tools that haven't been called + uncalled = tool_rules_solver.get_uncalled_required_tools(available_tools=set([t.name for t in self.agent_state.tools])) + if uncalled: + heartbeat_reason = ( + f"{NON_USER_MSG_PREFIX}ToolRuleViolated: You must call {', '.join(uncalled)} at least once to exit the loop." + ) + from letta.server.rest_api.utils import create_heartbeat_system_message + + heartbeat_msg = create_heartbeat_system_message( + agent_id=self.agent_state.id, + model=self.agent_state.llm_config.model, + function_call_success=True, + timezone=self.agent_state.timezone, + heartbeat_reason=heartbeat_reason, + run_id=run_id, + ) + messages_to_persist = (initial_messages or []) + [heartbeat_msg] + continue_stepping, stop_reason = True, None + else: + # No required tools remaining, end turn without persisting no-op + continue_stepping = False + stop_reason = LettaStopReason(stop_reason=StopReasonType.end_turn.value) + messages_to_persist = initial_messages or [] + + # Case 1b: No tool call but has content + else: + continue_stepping, heartbeat_reason, stop_reason = self._decide_continuation( + agent_state=self.agent_state, + tool_call_name=None, + tool_rule_violated=False, + tool_rules_solver=tool_rules_solver, + is_final_step=is_final_step, + finish_reason=finish_reason, + ) + assistant_message = create_letta_messages_from_llm_response( + agent_id=self.agent_state.id, + model=self.agent_state.llm_config.model, + function_name=None, + function_arguments=None, + tool_execution_result=None, + tool_call_id=None, + function_response=None, + timezone=self.agent_state.timezone, + continue_stepping=continue_stepping, + heartbeat_reason=heartbeat_reason, + reasoning_content=content, + pre_computed_assistant_message_id=pre_computed_assistant_message_id, + step_id=step_id, + run_id=run_id, + is_approval_response=is_approval_response, + force_set_request_heartbeat=False, + add_heartbeat_on_continue=bool(heartbeat_reason), + ) + messages_to_persist = (initial_messages or []) + assistant_message + return messages_to_persist, continue_stepping, stop_reason + + # 2. Check whether tool call requires approval (includes client-side tools) + if not is_approval_response: + # Get names of client-side tools (these are executed by client, not server) + client_tool_names = {ct.name for ct in self.client_tools} if self.client_tools else set() + + # Tools requiring approval: requires_approval tools OR client-side tools + requested_tool_calls = [ + t + for t in tool_calls + if tool_rules_solver.is_requires_approval_tool(t.function.name) or t.function.name in client_tool_names + ] + allowed_tool_calls = [ + t + for t in tool_calls + if not tool_rules_solver.is_requires_approval_tool(t.function.name) and t.function.name not in client_tool_names + ] + if requested_tool_calls: + approval_messages = create_approval_request_message_from_llm_response( + agent_id=self.agent_state.id, + model=self.agent_state.llm_config.model, + requested_tool_calls=requested_tool_calls, + allowed_tool_calls=allowed_tool_calls, + reasoning_content=content, + pre_computed_assistant_message_id=pre_computed_assistant_message_id, + step_id=step_id, + run_id=run_id, + ) + messages_to_persist = (initial_messages or []) + approval_messages + return messages_to_persist, False, LettaStopReason(stop_reason=StopReasonType.requires_approval.value) + + result_tool_returns = [] + + # 3. Handle client side tool execution + if tool_returns: + # Clamp client-side tool returns before persisting (JSON-aware: truncate only the 'message' field) + try: + cap = self._compute_tool_return_truncation_chars() + except Exception: + cap = 5000 + + for tr in tool_returns: + try: + if tr.func_response and isinstance(tr.func_response, str): + parsed = json.loads(tr.func_response) + if isinstance(parsed, dict) and "message" in parsed and isinstance(parsed["message"], str): + msg = parsed["message"] + if len(msg) > cap: + original_len = len(msg) + parsed["message"] = msg[:cap] + f"... [truncated {original_len - cap} chars]" + tr.func_response = json.dumps(parsed) + self.logger.warning(f"Truncated client-side tool return message from {original_len} to {cap} chars") + else: + # Fallback to raw string truncation if not a dict with 'message' + if len(tr.func_response) > cap: + original_len = len(tr.func_response) + tr.func_response = tr.func_response[:cap] + f"... [truncated {original_len - cap} chars]" + self.logger.warning(f"Truncated client-side tool return (raw) from {original_len} to {cap} chars") + except json.JSONDecodeError: + # Non-JSON or unexpected shape; truncate as raw string + if tr.func_response and len(tr.func_response) > cap: + original_len = len(tr.func_response) + tr.func_response = tr.func_response[:cap] + f"... [truncated {original_len - cap} chars]" + self.logger.warning(f"Truncated client-side tool return (non-JSON) from {original_len} to {cap} chars") + except Exception as e: + # Unexpected error; log and skip truncation for this return + self.logger.warning(f"Failed to truncate client-side tool return: {e}") + + continue_stepping = True + stop_reason = None + result_tool_returns = tool_returns + + # 4. Handle denial cases + if tool_call_denials: + # Convert ToolCallDenial objects to ToolReturn objects using shared helper + # Group denials by reason to potentially batch them, but for now process individually + for tool_call_denial in tool_call_denials: + denial_returns = create_tool_returns_for_denials( + tool_calls=[tool_call_denial], + denial_reason=tool_call_denial.reason, + timezone=self.agent_state.timezone, + ) + result_tool_returns.extend(denial_returns) + + # 5. Unified tool execution path (works for both single and multiple tools) + + # 5. Unified tool execution path (works for both single and multiple tools) + # Note: Parallel tool calling with tool rules is validated at agent create/update time. + # At runtime, we trust that if tool_rules exist, parallel_tool_calls=false is enforced earlier. + + # 5a. Prepare execution specs for all tools + exec_specs = [] + for tc in tool_calls: + call_id = tc.id or f"call_{uuid.uuid4().hex[:8]}" + name = tc.function.name + args = _safe_load_tool_call_str(tc.function.arguments) + args.pop(REQUEST_HEARTBEAT_PARAM, None) + args.pop(INNER_THOUGHTS_KWARG, None) + + # Validate against allowed tools + tool_rule_violated = name not in valid_tool_names and not is_approval_response + + # Handle prefilled args if present + if not tool_rule_violated: + prefill_args = tool_rules_solver.last_prefilled_args_by_tool.get(name) + if prefill_args: + target_tool = next((t for t in self.agent_state.tools if t.name == name), None) + provenance = tool_rules_solver.last_prefilled_args_provenance.get(name) + try: + args = merge_and_validate_prefilled_args( + tool=target_tool, + llm_args=args, + prefilled_args=prefill_args, + ) + except ValueError as ve: + # Invalid prefilled args - create error result + error_prefix = "Invalid prefilled tool arguments from tool rules" + prov_suffix = f" (source={provenance})" if provenance else "" + err_msg = f"{error_prefix}{prov_suffix}: {str(ve)}" + + exec_specs.append( + { + "id": call_id, + "name": name, + "args": args, + "violated": False, + "error": err_msg, + } + ) + continue + + exec_specs.append( + { + "id": call_id, + "name": name, + "args": args, + "violated": tool_rule_violated, + "error": None, + } + ) + + # 5c. Execute tools (sequentially for single, parallel for multiple) + async def _run_one(spec: Dict[str, Any]): + if spec.get("error"): + return ToolExecutionResult(status="error", func_return=spec["error"]), 0 + if spec["violated"]: + result = _build_rule_violation_result(spec["name"], valid_tool_names, tool_rules_solver) + return result, 0 + t0 = get_utc_timestamp_ns() + target_tool = next((x for x in self.agent_state.tools if x.name == spec["name"]), None) + res = await self._execute_tool( + target_tool=target_tool, + tool_args=spec["args"], + agent_state=self.agent_state, + agent_step_span=agent_step_span, + step_id=step_id, + ) + dt = get_utc_timestamp_ns() - t0 + return res, dt + + if len(exec_specs) == 1: + results = [await _run_one(exec_specs[0])] + else: + # separate tools by parallel execution capability + parallel_items = [] + serial_items = [] + + for idx, spec in enumerate(exec_specs): + target_tool = next((x for x in self.agent_state.tools if x.name == spec["name"]), None) + if target_tool and target_tool.enable_parallel_execution: + parallel_items.append((idx, spec)) + else: + serial_items.append((idx, spec)) + + # execute all parallel tools concurrently and all serial tools sequentially + results = [None] * len(exec_specs) + + parallel_results = await asyncio.gather(*[_run_one(spec) for _, spec in parallel_items]) if parallel_items else [] + for (idx, _), result in zip(parallel_items, parallel_results): + results[idx] = result + + for idx, spec in serial_items: + results[idx] = await _run_one(spec) + + # 5d. Update metrics with execution time + if step_metrics is not None and results: + step_metrics.tool_execution_ns = max(dt for _, dt in results) + + # 5e. Process results and compute function responses + function_responses: list[Optional[str]] = [] + persisted_continue_flags: list[bool] = [] + persisted_stop_reasons: list[LettaStopReason | None] = [] + + for idx, spec in enumerate(exec_specs): + tool_execution_result, _ = results[idx] + has_prefill_error = bool(spec.get("error")) + + # Validate and format function response + truncate = spec["name"] not in {"conversation_search", "conversation_search_date", "archival_memory_search"} + return_char_limit = next((t.return_char_limit for t in self.agent_state.tools if t.name == spec["name"]), None) + function_response_string = validate_function_response( + tool_execution_result.func_return, + return_char_limit=return_char_limit, + truncate=truncate, + ) + function_responses.append(function_response_string) + + # Update last function response (for tool rules) + self.last_function_response = package_function_response( + was_success=tool_execution_result.success_flag, + response_string=function_response_string, + timezone=self.agent_state.timezone, + ) + + # Register successful tool call with solver + if not spec["violated"] and not has_prefill_error: + tool_rules_solver.register_tool_call(spec["name"]) + + # Decide continuation for this tool + if has_prefill_error: + cont = False + _hb_reason = None + sr = LettaStopReason(stop_reason=StopReasonType.invalid_tool_call.value) + else: + cont, _hb_reason, sr = self._decide_continuation( + agent_state=self.agent_state, + tool_call_name=spec["name"], + tool_rule_violated=spec["violated"], + tool_rules_solver=tool_rules_solver, + is_final_step=(is_final_step and idx == len(exec_specs) - 1), + finish_reason=finish_reason, + ) + persisted_continue_flags.append(cont) + persisted_stop_reasons.append(sr) + + # 5f. Create messages using parallel message creation (works for both single and multi) + tool_call_specs = [{"name": s["name"], "arguments": s["args"], "id": s["id"]} for s in exec_specs] + tool_execution_results = [res for (res, _) in results] + + # Use the parallel message creation function for both single and multiple tools + parallel_messages = create_parallel_tool_messages_from_llm_response( + agent_id=self.agent_state.id, + model=self.agent_state.llm_config.model, + tool_call_specs=tool_call_specs, + tool_execution_results=tool_execution_results, + function_responses=function_responses, + timezone=self.agent_state.timezone, + run_id=run_id, + step_id=step_id, + reasoning_content=content, + pre_computed_assistant_message_id=pre_computed_assistant_message_id, + is_approval_response=is_approval_response, + tool_returns=result_tool_returns, + ) + + messages_to_persist: list[Message] = (initial_messages or []) + parallel_messages + + # Set run_id and step_id on all messages before persisting + for message in messages_to_persist: + if message.run_id is None: + message.run_id = run_id + if message.step_id is None: + message.step_id = step_id + + # 5g. Aggregate continuation decisions + aggregate_continue = any(persisted_continue_flags) if persisted_continue_flags else False + aggregate_continue = aggregate_continue or tool_call_denials or tool_returns + + # Determine aggregate stop reason + aggregate_stop_reason = None + for sr in persisted_stop_reasons: + if sr is not None: + aggregate_stop_reason = sr + + # For parallel tool calls, always continue to allow the agent to process/summarize results + # unless a terminal tool was called or we hit max steps + if len(exec_specs) > 1: + has_terminal = any(sr and sr.stop_reason == StopReasonType.tool_rule.value for sr in persisted_stop_reasons) + is_max_steps = any(sr and sr.stop_reason == StopReasonType.max_steps.value for sr in persisted_stop_reasons) + + if not has_terminal and not is_max_steps: + # Force continuation for parallel tool execution + aggregate_continue = True + aggregate_stop_reason = None + return messages_to_persist, aggregate_continue, aggregate_stop_reason + + @trace_method + def _decide_continuation( + self, + agent_state: AgentState, + tool_call_name: Optional[str], + tool_rule_violated: bool, + tool_rules_solver: ToolRulesSolver, + is_final_step: bool | None, + finish_reason: str | None = None, + ) -> tuple[bool, str | None, LettaStopReason | None]: + """ + In v3 loop, we apply the following rules: + + 1. Did not call a tool? Loop ends + + 2. Called a tool? Loop continues. This can be: + 2a. Called tool, tool executed successfully + 2b. Called tool, tool failed to execute + 2c. Called tool + tool rule violation (did not execute) + + """ + continue_stepping = True # Default continue + continuation_reason: str | None = None + stop_reason: LettaStopReason | None = None + + if tool_call_name is None: + # No tool call – if there are required-before-exit tools uncalled, keep stepping + # and provide explicit feedback to the model; otherwise end the loop. + uncalled = tool_rules_solver.get_uncalled_required_tools(available_tools=set([t.name for t in agent_state.tools])) + if uncalled and not is_final_step: + reason = f"{NON_USER_MSG_PREFIX}ToolRuleViolated: You must call {', '.join(uncalled)} at least once to exit the loop." + return True, reason, None + # No required tools remaining → end turn + # Check if the LLM hit max_tokens (finish_reason == "length") + if finish_reason == "length": + return False, None, LettaStopReason(stop_reason=StopReasonType.max_tokens_exceeded.value) + return False, None, LettaStopReason(stop_reason=StopReasonType.end_turn.value) + else: + if tool_rule_violated: + continue_stepping = True + continuation_reason = f"{NON_USER_MSG_PREFIX}Continuing: tool rule violation." + else: + tool_rules_solver.register_tool_call(tool_call_name) + + if tool_rules_solver.is_terminal_tool(tool_call_name): + stop_reason = LettaStopReason(stop_reason=StopReasonType.tool_rule.value) + continue_stepping = False + + elif tool_rules_solver.has_children_tools(tool_call_name): + continue_stepping = True + continuation_reason = f"{NON_USER_MSG_PREFIX}Continuing: child tool rule." + + elif tool_rules_solver.is_continue_tool(tool_call_name): + continue_stepping = True + continuation_reason = f"{NON_USER_MSG_PREFIX}Continuing: continue tool rule." + + # – hard stop overrides – + if is_final_step: + continue_stepping = False + stop_reason = LettaStopReason(stop_reason=StopReasonType.max_steps.value) + else: + uncalled = tool_rules_solver.get_uncalled_required_tools(available_tools=set([t.name for t in agent_state.tools])) + if uncalled: + continue_stepping = True + continuation_reason = ( + f"{NON_USER_MSG_PREFIX}Continuing, user expects these tools: [{', '.join(uncalled)}] to be called still." + ) + + stop_reason = None # reset – we’re still going + + return continue_stepping, continuation_reason, stop_reason + + @trace_method + async def _get_valid_tools(self): + tools = self.agent_state.tools + valid_tool_names = self.tool_rules_solver.get_allowed_tool_names( + available_tools=set([t.name for t in tools]), + last_function_response=self.last_function_response, + error_on_empty=False, # Return empty list instead of raising error + ) or list(set(t.name for t in tools)) + + # Get client tool names to filter out server tools with same name (client tools override) + client_tool_names = {ct.name for ct in self.client_tools} if self.client_tools else set() + + # Build allowed tools from server tools, excluding those overridden by client tools + allowed_tools = [ + enable_strict_mode(t.json_schema, strict=self.agent_state.llm_config.strict) + for t in tools + if t.name in set(valid_tool_names) and t.name not in client_tool_names + ] + + # Merge client-side tools (use flat format matching enable_strict_mode output) + if self.client_tools: + for ct in self.client_tools: + client_tool_schema = { + "name": ct.name, + "description": ct.description, + "parameters": ct.parameters or {"type": "object", "properties": {}}, + } + allowed_tools.append(client_tool_schema) + + terminal_tool_names = {rule.tool_name for rule in self.tool_rules_solver.terminal_tool_rules} + allowed_tools = runtime_override_tool_json_schema( + tool_list=allowed_tools, + response_format=self.agent_state.response_format, + request_heartbeat=False, # NOTE: difference for v3 (don't add request heartbeat) + terminal_tools=terminal_tool_names, + ) + return allowed_tools + + @trace_method + async def compact( + self, + messages, + trigger_threshold: Optional[int] = None, + compaction_settings: Optional["CompactionSettings"] = None, + run_id: Optional[str] = None, + step_id: Optional[str] = None, + use_summary_role: bool = False, + trigger: Optional[str] = None, + context_tokens_before: Optional[int] = None, + messages_count_before: Optional[int] = None, + billing_context: Optional["BillingContext"] = None, + ) -> tuple[Message, list[Message], str]: + """Compact the current in-context messages for this agent. + + Compaction uses a summarizer LLM configuration derived from + ``compaction_settings.model`` when provided. This mirrors how agent + creation derives defaults from provider-specific ModelSettings, but is + localized to summarization. + + Args: + use_summary_role: If True, the summary message will be created with + role=summary instead of role=user. This enables first-class + summary message handling in the database and API responses. + trigger: What triggered the compaction (e.g., "context_window_exceeded", "post_step_context_check"). + context_tokens_before: Token count before compaction (for stats). + messages_count_before: Message count before compaction (for stats). + """ + + # Determine compaction settings: passed-in > agent's > global defaults + effective_compaction_settings = compaction_settings or self.agent_state.compaction_settings + + result = await compact_messages( + actor=self.actor, + agent_id=self.agent_state.id, + agent_llm_config=self.agent_state.llm_config, + telemetry_manager=self.telemetry_manager, + llm_client=self.llm_client, + agent_type=self.agent_state.agent_type, + messages=messages, + timezone=self.agent_state.timezone, + compaction_settings=effective_compaction_settings, + agent_tags=self.agent_state.tags, + tools=await self._get_valid_tools(), # Pass json schemas including client tools for cache compatibility (for self compaction) + trigger_threshold=trigger_threshold, + run_id=run_id, + step_id=step_id, + use_summary_role=use_summary_role, + trigger=trigger, + context_tokens_before=context_tokens_before, + messages_count_before=messages_count_before, + billing_context=billing_context, + ) + + # Update the agent's context token estimate + self.context_token_estimate = result.context_token_estimate + + return result.summary_message, result.compacted_messages, result.summary_text diff --git a/letta/agents/voice_agent.py b/letta/agents/voice_agent.py new file mode 100644 index 0000000..86f75fa --- /dev/null +++ b/letta/agents/voice_agent.py @@ -0,0 +1,525 @@ +import json +import uuid +from datetime import datetime, timedelta, timezone +from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Optional + +import openai + +if TYPE_CHECKING: + from letta.schemas.tool_execution_result import ToolExecutionResult + +from letta.agents.base_agent import BaseAgent +from letta.agents.exceptions import IncompatibleAgentType +from letta.agents.voice_sleeptime_agent import VoiceSleeptimeAgent +from letta.constants import DEFAULT_MAX_STEPS, NON_USER_MSG_PREFIX, PRE_EXECUTION_MESSAGE_ARG, REQUEST_HEARTBEAT_PARAM +from letta.helpers.datetime_helpers import get_utc_time +from letta.helpers.tool_execution_helper import add_pre_execution_message, enable_strict_mode, remove_request_heartbeat +from letta.interfaces.openai_chat_completions_streaming_interface import OpenAIChatCompletionsStreamingInterface +from letta.log import get_logger +from letta.prompts.prompt_generator import PromptGenerator +from letta.schemas.agent import AgentState +from letta.schemas.enums import AgentType, MessageRole, ToolType +from letta.schemas.letta_response import LettaResponse +from letta.schemas.message import Message, MessageCreate +from letta.schemas.openai.chat_completion_request import ( + AssistantMessage, + ChatCompletionRequest, + Tool, + ToolCall, + ToolCallFunction, + ToolMessage, + UserMessage, +) +from letta.schemas.user import User +from letta.server.rest_api.utils import ( + convert_in_context_letta_messages_to_openai, + create_assistant_messages_from_openai_response, + create_input_messages, + create_letta_messages_from_llm_response, +) +from letta.services.agent_manager import AgentManager +from letta.services.block_manager import BlockManager +from letta.services.message_manager import MessageManager +from letta.services.passage_manager import PassageManager +from letta.services.run_manager import RunManager +from letta.services.summarizer.enums import SummarizationMode +from letta.services.summarizer.summarizer import Summarizer +from letta.services.tool_executor.tool_execution_manager import ToolExecutionManager +from letta.settings import model_settings + +logger = get_logger(__name__) + + +class VoiceAgent(BaseAgent): + """ + A function-calling loop for streaming OpenAI responses with tool execution. + This agent: + - Streams partial tokens in real-time for low-latency output. + - Detects tool calls and invokes external tools. + - Gracefully handles OpenAI API failures (429, etc.) and streams errors. + """ + + def __init__( + self, + agent_id: str, + openai_client: openai.AsyncClient, + message_manager: MessageManager, + agent_manager: AgentManager, + block_manager: BlockManager, + run_manager: RunManager, + passage_manager: PassageManager, + actor: User, + ): + super().__init__( + agent_id=agent_id, openai_client=openai_client, message_manager=message_manager, agent_manager=agent_manager, actor=actor + ) + + # Summarizer settings + self.block_manager = block_manager + self.run_manager = run_manager + self.passage_manager = passage_manager + # TODO: This is not guaranteed to exist! + self.summary_block_label = "human" + + # Cached archival memory/message size + self.num_messages = None + self.num_archival_memories = None + + def init_summarizer(self, agent_state: AgentState) -> Summarizer: + if not agent_state.multi_agent_group: + raise ValueError("Low latency voice agent is not part of a multiagent group, missing sleeptime agent.") + if len(agent_state.multi_agent_group.agent_ids) != 1: + raise ValueError( + f"None or multiple participant agents found in voice sleeptime group: {agent_state.multi_agent_group.agent_ids}" + ) + voice_sleeptime_agent_id = agent_state.multi_agent_group.agent_ids[0] + summarizer = Summarizer( + mode=SummarizationMode.STATIC_MESSAGE_BUFFER, + summarizer_agent=VoiceSleeptimeAgent( + agent_id=voice_sleeptime_agent_id, + convo_agent_state=agent_state, + message_manager=self.message_manager, + agent_manager=self.agent_manager, + actor=self.actor, + block_manager=self.block_manager, + run_manager=self.run_manager, + passage_manager=self.passage_manager, + target_block_label=self.summary_block_label, + ), + message_buffer_limit=agent_state.multi_agent_group.max_message_buffer_length, + message_buffer_min=agent_state.multi_agent_group.min_message_buffer_length, + ) + + return summarizer + + async def step(self, input_messages: List[MessageCreate], max_steps: int = DEFAULT_MAX_STEPS) -> LettaResponse: + raise NotImplementedError("VoiceAgent does not have a synchronous step implemented currently.") + + async def step_stream(self, input_messages: List[MessageCreate], max_steps: int = DEFAULT_MAX_STEPS) -> AsyncGenerator[str, None]: + """ + Main streaming loop that yields partial tokens. + Whenever we detect a tool call, we yield from _handle_ai_response as well. + """ + if len(input_messages) != 1 or input_messages[0].role != MessageRole.user: + raise ValueError(f"Voice Agent was invoked with multiple input messages or message did not have role `user`: {input_messages}") + + user_query = input_messages[0].content[0].text + + agent_state = await self.agent_manager.get_agent_by_id_async( + agent_id=self.agent_id, + include_relationships=["tools", "memory", "tool_exec_environment_variables", "multi_agent_group"], + actor=self.actor, + ) + + # TODO: Refactor this so it uses our in-house clients + # TODO: For now, piggyback off of OpenAI client for ease + if agent_state.llm_config.model_endpoint_type == "anthropic": + self.openai_client.api_key = model_settings.anthropic_api_key + self.openai_client.base_url = "https://api.anthropic.com/v1/" + elif agent_state.llm_config.model_endpoint_type != "openai": + raise ValueError("Letta voice agents are only compatible with OpenAI or Anthropic.") + + # Safety check + if agent_state.agent_type != AgentType.voice_convo_agent: + raise IncompatibleAgentType(expected_type=AgentType.voice_convo_agent, actual_type=agent_state.agent_type) + + summarizer = self.init_summarizer(agent_state=agent_state) + + in_context_messages = await self.message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=self.actor) + memory_edit_timestamp = get_utc_time() + in_context_messages[0].content[0].text = await PromptGenerator.compile_system_message_async( + system_prompt=agent_state.system, + in_context_memory=agent_state.memory, + agent_id=agent_state.id, + conversation_id="default", + in_context_memory_last_edit=memory_edit_timestamp, + timezone=agent_state.timezone, + previous_message_count=self.num_messages, + archival_memory_size=self.num_archival_memories, + sources=agent_state.sources, + max_files_open=agent_state.max_files_open, + llm_config=agent_state.llm_config, + ) + letta_message_db_queue = await create_input_messages( + input_messages=input_messages, agent_id=agent_state.id, timezone=agent_state.timezone, run_id=None, actor=self.actor + ) + in_memory_message_history = self.pre_process_input_message(input_messages) + + # TODO: Define max steps here + for _ in range(max_steps): + # Rebuild memory each loop + in_context_messages = await self._rebuild_memory_async(in_context_messages, agent_state) + openai_messages = convert_in_context_letta_messages_to_openai(in_context_messages, exclude_system_messages=True) + openai_messages.extend(in_memory_message_history) + + request = self._build_openai_request(openai_messages, agent_state) + + stream = await self.openai_client.chat.completions.create(**request.model_dump(exclude_unset=True)) + streaming_interface = OpenAIChatCompletionsStreamingInterface(stream_pre_execution_message=True) + + # 1) Yield partial tokens from OpenAI + async for sse_chunk in streaming_interface.process(stream): + yield sse_chunk + + # 2) Now handle the final AI response. This might yield more text (stalling, etc.) + should_continue = await self._handle_ai_response( + user_query, + streaming_interface, + agent_state, + in_memory_message_history, + letta_message_db_queue, + ) + + if not should_continue: + break + + # Rebuild context window if desired + await self._rebuild_context_window(summarizer, in_context_messages, letta_message_db_queue) + + yield "data: [DONE]\n\n" + + async def _handle_ai_response( + self, + user_query: str, + streaming_interface: "OpenAIChatCompletionsStreamingInterface", + agent_state: AgentState, + in_memory_message_history: List[Dict[str, Any]], + letta_message_db_queue: List[Any], + ) -> bool: + """ + Now that streaming is done, handle the final AI response. + This might yield additional SSE tokens if we do stalling. + At the end, set self._continue_execution accordingly. + """ + # 1. If we have any leftover content from partial stream, store it as an assistant message + if streaming_interface.content_buffer: + content = "".join(streaming_interface.content_buffer) + in_memory_message_history.append({"role": "assistant", "content": content}) + + assistant_msgs = create_assistant_messages_from_openai_response( + response_text=content, + agent_id=agent_state.id, + model=agent_state.llm_config.model, + timezone=agent_state.timezone, + ) + letta_message_db_queue.extend(assistant_msgs) + + # 2. If a tool call was requested, handle it + if streaming_interface.tool_call_happened: + tool_call_name = streaming_interface.tool_call_name + tool_call_args_str = streaming_interface.tool_call_args_str or "{}" + try: + tool_args = json.loads(tool_call_args_str) + except json.JSONDecodeError: + tool_args = {} + + tool_call_id = streaming_interface.tool_call_id or f"call_{uuid.uuid4().hex[:8]}" + assistant_tool_call_msg = AssistantMessage( + content=None, + tool_calls=[ + ToolCall( + id=tool_call_id, + function=ToolCallFunction( + name=tool_call_name, + arguments=tool_call_args_str, + ), + ) + ], + ) + in_memory_message_history.append(assistant_tool_call_msg.model_dump()) + + tool_execution_result = await self._execute_tool( + user_query=user_query, + tool_name=tool_call_name, + tool_args=tool_args, + agent_state=agent_state, + ) + tool_result = tool_execution_result.func_return + + # 3. Provide function_call response back into the conversation + # TODO: fix this tool format + tool_message = ToolMessage( + content=json.dumps({"result": tool_result}), + tool_call_id=tool_call_id, + ) + in_memory_message_history.append(tool_message.model_dump()) + + # 4. Insert heartbeat message for follow-up + heartbeat_user_message = UserMessage( + content=f"{NON_USER_MSG_PREFIX} Tool finished executing. Summarize the result for the user." + ) + in_memory_message_history.append(heartbeat_user_message.model_dump()) + + # 5. Also store in DB + tool_call_messages = create_letta_messages_from_llm_response( + agent_id=agent_state.id, + model=agent_state.llm_config.model, + function_name=tool_call_name, + function_arguments=tool_args, + tool_call_id=tool_call_id, + function_response=tool_result, + tool_execution_result=tool_execution_result, + timezone=agent_state.timezone, + continue_stepping=True, + ) + letta_message_db_queue.extend(tool_call_messages) + + # Because we have new data, we want to continue the while-loop in `step_stream` + return True + else: + # If we got here, there's no tool call. If finish_reason_stop => done + return not streaming_interface.finish_reason_stop + + async def _rebuild_context_window( + self, summarizer: Summarizer, in_context_messages: List[Message], letta_message_db_queue: List[Message] + ) -> None: + new_letta_messages = await self.message_manager.create_many_messages_async(letta_message_db_queue, actor=self.actor) + + # TODO: Make this more general and configurable, less brittle + new_in_context_messages, _updated = await summarizer.summarize( + in_context_messages=in_context_messages, new_letta_messages=new_letta_messages + ) + + await self.agent_manager.update_message_ids_async( + agent_id=self.agent_id, message_ids=[m.id for m in new_in_context_messages], actor=self.actor + ) + + async def _rebuild_memory_async( + self, + in_context_messages: List[Message], + agent_state: AgentState, + ) -> List[Message]: + if not self.num_messages: + self.num_messages = await self.message_manager.size_async( + agent_id=agent_state.id, + actor=self.actor, + ) + if not self.num_archival_memories: + self.num_archival_memories = await self.passage_manager.agent_passage_size_async( + agent_id=agent_state.id, + actor=self.actor, + ) + + return await super()._rebuild_memory_async( + in_context_messages, agent_state, num_messages=self.num_messages, num_archival_memories=self.num_archival_memories + ) + + def _build_openai_request(self, openai_messages: List[Dict], agent_state: AgentState) -> ChatCompletionRequest: + tool_schemas = self._build_tool_schemas(agent_state) + tool_choice = "auto" if tool_schemas else None + + openai_request = ChatCompletionRequest( + model=agent_state.llm_config.model, + messages=openai_messages, + tools=self._build_tool_schemas(agent_state), + tool_choice=tool_choice, + user=self.actor.id, + max_completion_tokens=agent_state.llm_config.max_tokens, + temperature=agent_state.llm_config.temperature, + stream=True, + ) + return openai_request + + def _build_tool_schemas(self, agent_state: AgentState, external_tools_only=True) -> List[Tool]: + if external_tools_only: + tools = [ + t + for t in agent_state.tools + if t.tool_type in {ToolType.CUSTOM, ToolType.LETTA_FILES_CORE, ToolType.LETTA_BUILTIN, ToolType.EXTERNAL_MCP} + ] + else: + tools = agent_state.tools + + # Special tool state + search_memory_utterance_description = ( + "A lengthier message to be uttered while your memories of the current conversation are being re-contextualized." + "You MUST also include punctuation at the end of this message." + "For example: 'Let me double-check my notes—one moment, please.'" + ) + + strict = agent_state.llm_config.strict + search_memory_json = Tool( + type="function", + function=enable_strict_mode( # strict mode based on config + add_pre_execution_message( # injects pre_exec_msg ✓ + { + "name": "search_memory", + "description": ( + "Look in long-term or earlier-conversation memory **only when** the " + "user asks about something missing from the visible context. " + "The user's latest utterance is sent automatically as the main query.\n\n" + "Optional refinements (set unused fields to *null*):\n" + "• `convo_keyword_queries` – extra names/IDs if the request is vague.\n" + "• `start_minutes_ago` / `end_minutes_ago` – limit results to a recent time window." + ), + "parameters": { + "type": "object", + "properties": { + "convo_keyword_queries": { + "type": ["array", "null"], + "items": {"type": "string"}, + "description": ( + "Extra keywords (e.g., order ID, place name). Use *null* when the utterance is already specific." + ), + }, + "start_minutes_ago": { + "type": ["integer", "null"], + "description": ( + "Newer bound of the time window, in minutes ago. Use *null* if no lower bound is needed." + ), + }, + "end_minutes_ago": { + "type": ["integer", "null"], + "description": ( + "Older bound of the time window, in minutes ago. Use *null* if no upper bound is needed." + ), + }, + }, + "required": [ + "convo_keyword_queries", + "start_minutes_ago", + "end_minutes_ago", + ], + "additionalProperties": False, + }, + }, + description=search_memory_utterance_description, + ), + strict=strict, + ), + ) + + # TODO: Customize whether or not to have heartbeats, pre_exec_message, etc. + return [search_memory_json] + [ + Tool( + type="function", + function=enable_strict_mode(add_pre_execution_message(remove_request_heartbeat(t.json_schema)), strict=strict), + ) + for t in tools + ] + + async def _execute_tool(self, user_query: str, tool_name: str, tool_args: dict, agent_state: AgentState) -> "ToolExecutionResult": + """ + Executes a tool and returns the ToolExecutionResult. + """ + from letta.schemas.tool_execution_result import ToolExecutionResult + + # Special memory case + if tool_name == "search_memory": + tool_result = await self._search_memory( + archival_query=user_query, + convo_keyword_queries=tool_args["convo_keyword_queries"], + start_minutes_ago=tool_args["start_minutes_ago"], + end_minutes_ago=tool_args["end_minutes_ago"], + agent_state=agent_state, + ) + return ToolExecutionResult( + func_return=tool_result, + status="success", + ) + + # Find the target tool + target_tool = next((x for x in agent_state.tools if x.name == tool_name), None) + if not target_tool: + return ToolExecutionResult( + func_return=f"Tool {tool_name} not found", + status="error", + ) + + # Use ToolExecutionManager for modern tool execution + # Use pre-decrypted environment variable values (populated in from_orm_async) + sandbox_env_vars = {var.key: var.value or "" for var in agent_state.secrets} + tool_execution_manager = ToolExecutionManager( + agent_state=agent_state, + message_manager=self.message_manager, + agent_manager=self.agent_manager, + block_manager=self.block_manager, + run_manager=self.run_manager, + passage_manager=self.passage_manager, + sandbox_env_vars=sandbox_env_vars, + actor=self.actor, + ) + + # Remove request heartbeat / pre_exec_message + tool_args.pop(PRE_EXECUTION_MESSAGE_ARG, None) + tool_args.pop(REQUEST_HEARTBEAT_PARAM, None) + + tool_execution_result = await tool_execution_manager.execute_tool_async( + function_name=tool_name, + function_args=tool_args, + tool=target_tool, + step_id=None, # VoiceAgent doesn't use step tracking currently + ) + + return tool_execution_result + + async def _search_memory( + self, + archival_query: str, + agent_state: AgentState, + convo_keyword_queries: Optional[List[str]] = None, + start_minutes_ago: Optional[int] = None, + end_minutes_ago: Optional[int] = None, + ) -> str: + # Retrieve from archival memory + now = datetime.now(timezone.utc) + start_date = now - timedelta(minutes=end_minutes_ago) if end_minutes_ago is not None else None + end_date = now - timedelta(minutes=start_minutes_ago) if start_minutes_ago is not None else None + + # If both bounds exist but got reversed, swap them + # Shouldn't happen, but in case LLM misunderstands + if start_date and end_date and start_date > end_date: + start_date, end_date = end_date, start_date + + archival_results = await self.agent_manager.query_agent_passages_async( + actor=self.actor, + agent_id=self.agent_id, + query_text=archival_query, + limit=5, + embedding_config=agent_state.embedding_config, + embed_query=True, + start_date=start_date, + end_date=end_date, + ) + # Extract passages from tuples and format + formatted_archival_results = [{"timestamp": str(passage.created_at), "content": passage.text} for passage, _, _ in archival_results] + response = { + "archival_search_results": formatted_archival_results, + } + + # Retrieve from conversation + keyword_results = {} + if convo_keyword_queries: + for keyword in convo_keyword_queries: + messages = await self.message_manager.list_messages( + agent_id=self.agent_id, + actor=self.actor, + query_text=keyword, + limit=3, + ) + if messages: + keyword_results[keyword] = [message.content[0].text for message in messages] + + response["convo_keyword_search_results"] = keyword_results + + return json.dumps(response, indent=2) diff --git a/letta/agents/voice_sleeptime_agent.py b/letta/agents/voice_sleeptime_agent.py new file mode 100644 index 0000000..6f7f184 --- /dev/null +++ b/letta/agents/voice_sleeptime_agent.py @@ -0,0 +1,193 @@ +from typing import TYPE_CHECKING, AsyncGenerator, List, Optional, Tuple, Union + +if TYPE_CHECKING: + from opentelemetry.trace import Span + + from letta.schemas.tool_execution_result import ToolExecutionResult + +from letta.agents.helpers import _create_letta_response, serialize_message_history +from letta.agents.letta_agent import LettaAgent +from letta.constants import DEFAULT_MAX_STEPS +from letta.otel.tracing import trace_method +from letta.schemas.agent import AgentState +from letta.schemas.block import BlockUpdate +from letta.schemas.enums import MessageStreamStatus, ToolType +from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, MessageType +from letta.schemas.letta_response import LettaResponse +from letta.schemas.message import MessageCreate +from letta.schemas.tool_rule import ChildToolRule, ContinueToolRule, InitToolRule, TerminalToolRule +from letta.schemas.user import User +from letta.services.agent_manager import AgentManager +from letta.services.block_manager import BlockManager +from letta.services.message_manager import MessageManager +from letta.services.passage_manager import PassageManager +from letta.services.run_manager import RunManager +from letta.services.summarizer.enums import SummarizationMode +from letta.services.summarizer.summarizer import Summarizer +from letta.types import JsonDict + + +class VoiceSleeptimeAgent(LettaAgent): + """ + A special variant of the LettaAgent that helps with offline memory computations specifically for voice. + """ + + def __init__( + self, + agent_id: str, + convo_agent_state: AgentState, + message_manager: MessageManager, + agent_manager: AgentManager, + block_manager: BlockManager, + run_manager: RunManager, + passage_manager: PassageManager, + target_block_label: str, + actor: User, + ): + super().__init__( + agent_id=agent_id, + message_manager=message_manager, + agent_manager=agent_manager, + block_manager=block_manager, + job_manager=run_manager, + passage_manager=passage_manager, + actor=actor, + ) + + self.convo_agent_state = convo_agent_state + self.target_block_label = target_block_label + self.message_transcripts = [] + self.summarizer = Summarizer( + mode=SummarizationMode.STATIC_MESSAGE_BUFFER, + summarizer_agent=None, + message_buffer_limit=20, + message_buffer_min=10, + ) + + def update_message_transcript(self, message_transcripts: List[str]): + self.message_transcripts = message_transcripts + + async def step( + self, + input_messages: List[MessageCreate], + max_steps: int = DEFAULT_MAX_STEPS, + run_id: Optional[str] = None, + use_assistant_message: bool = True, + request_start_timestamp_ns: Optional[int] = None, + include_return_message_types: Optional[List[MessageType]] = None, + ) -> LettaResponse: + """ + Process the user's input message, allowing the model to call memory-related tools + until it decides to stop and provide a final response. + """ + agent_state = self.agent_manager.get_agent_by_id(self.agent_id, actor=self.actor) + + # Add tool rules to the agent_state specifically for this type of agent + agent_state.tool_rules = [ + InitToolRule(tool_name="store_memories"), + ChildToolRule(tool_name="store_memories", children=["rethink_user_memory"]), + ContinueToolRule(tool_name="rethink_user_memory"), + TerminalToolRule(tool_name="finish_rethinking_memory"), + ] + + # Summarize + current_in_context_messages, new_in_context_messages, stop_reason, usage = await super()._step( + agent_state=agent_state, input_messages=input_messages, max_steps=max_steps + ) + new_in_context_messages, _updated = await self.summarizer.summarize( + in_context_messages=current_in_context_messages, new_letta_messages=new_in_context_messages + ) + self.agent_manager.set_in_context_messages( + agent_id=self.agent_id, message_ids=[m.id for m in new_in_context_messages], actor=self.actor + ) + + return _create_letta_response( + new_in_context_messages=new_in_context_messages, + use_assistant_message=use_assistant_message, + stop_reason=stop_reason, + usage=usage, + include_return_message_types=include_return_message_types, + ) + + @trace_method + async def _execute_tool( + self, + tool_name: str, + tool_args: JsonDict, + agent_state: AgentState, + agent_step_span: Optional["Span"] = None, + step_id: str | None = None, + ) -> "ToolExecutionResult": + """ + Executes a tool and returns the ToolExecutionResult + """ + from letta.schemas.tool_execution_result import ToolExecutionResult + + # Special memory case + target_tool = next((x for x in agent_state.tools if x.name == tool_name), None) + if not target_tool: + return ToolExecutionResult(status="error", func_return=f"Tool not found: {tool_name}") + + try: + if target_tool.name == "rethink_user_memory" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE: + func_return, success_flag = self.rethink_user_memory(agent_state=agent_state, **tool_args) + return ToolExecutionResult(func_return=func_return, status="success" if success_flag else "error") + elif target_tool.name == "finish_rethinking_memory" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE: + return ToolExecutionResult(func_return="", status="success") + elif target_tool.name == "store_memories" and target_tool.tool_type == ToolType.LETTA_VOICE_SLEEPTIME_CORE: + chunks = tool_args.get("chunks", []) + results = [self.store_memory(agent_state=self.convo_agent_state, **chunk_args) for chunk_args in chunks] + + aggregated_result = next((res for res, _ in results if res is not None), None) + aggregated_success = all(success for _, success in results) + + return ToolExecutionResult( + func_return=aggregated_result, status="success" if aggregated_success else "error" + ) # Note that here we store to the convo agent's archival memory + else: + result = f"Voice sleeptime agent tried invoking invalid tool with type {target_tool.tool_type}: {target_tool}" + return ToolExecutionResult(func_return=result, status="error") + except Exception as e: + return ToolExecutionResult(func_return=f"Failed to call tool. Error: {e}", status="error") + + def rethink_user_memory(self, new_memory: str, agent_state: AgentState) -> Tuple[str, bool]: + if agent_state.memory.get_block(self.target_block_label) is None: + agent_state.memory.create_block(label=self.target_block_label, value=new_memory) + + agent_state.memory.update_block_value(label=self.target_block_label, value=new_memory) + + target_block = agent_state.memory.get_block(self.target_block_label) + self.block_manager.update_block(block_id=target_block.id, block_update=BlockUpdate(value=target_block.value), actor=self.actor) + + return "", True + + def store_memory(self, start_index: int, end_index: int, context: str, agent_state: AgentState) -> Tuple[str, bool]: + """ + Store a memory. + """ + try: + messages = self.message_transcripts[start_index : end_index + 1] + memory = serialize_message_history(messages, context) + self.agent_manager.passage_manager.insert_passage( + agent_state=agent_state, + text=memory, + actor=self.actor, + ) + self.agent_manager.rebuild_system_prompt(agent_id=agent_state.id, actor=self.actor, force=True) + + return "", True + except Exception as e: + return f"Failed to store memory given start_index {start_index} and end_index {end_index}: {e}", False + + async def step_stream( + self, + input_messages: List[MessageCreate], + max_steps: int = DEFAULT_MAX_STEPS, + use_assistant_message: bool = True, + request_start_timestamp_ns: Optional[int] = None, + include_return_message_types: Optional[List[MessageType]] = None, + ) -> AsyncGenerator[Union[LettaMessage, LegacyLettaMessage, MessageStreamStatus], None]: + """ + This agent is synchronous-only. If called in an async context, raise an error. + """ + raise NotImplementedError("VoiceSleeptimeAgent does not support async step.") diff --git a/letta/cli/cli.py b/letta/cli/cli.py new file mode 100644 index 0000000..e566ca6 --- /dev/null +++ b/letta/cli/cli.py @@ -0,0 +1,48 @@ +import sys +from enum import Enum +from typing import Annotated, Optional + +import typer + +from letta.log import get_logger + +logger = get_logger(__name__) + + +class ServerChoice(Enum): + rest_api = "rest" + ws_api = "websocket" + + +def server( + type: Annotated[ServerChoice, typer.Option(help="Server to run")] = "rest", + port: Annotated[Optional[int], typer.Option(help="Port to run the server on")] = None, + host: Annotated[Optional[str], typer.Option(help="Host to run the server on (default to localhost)")] = None, + debug: Annotated[bool, typer.Option(help="Turn debugging output on")] = False, + reload: Annotated[bool, typer.Option(help="Enable hot-reload")] = False, + ade: Annotated[bool, typer.Option(help="Allows remote access")] = False, # NOTE: deprecated + secure: Annotated[bool, typer.Option(help="Adds simple security access")] = False, + localhttps: Annotated[bool, typer.Option(help="Setup local https")] = False, +): + """Launch a Letta server process""" + if type == ServerChoice.rest_api: + pass + + try: + from letta.server.rest_api.app import start_server + + start_server(port=port, host=host, debug=debug, reload=reload) + + except KeyboardInterrupt: + # Handle CTRL-C + typer.secho("Terminating the server...") + sys.exit(0) + + elif type == ServerChoice.ws_api: + raise NotImplementedError("WS suppport deprecated") + + +def version() -> str: + import letta + + print(letta.__version__) diff --git a/letta/cli/cli_load.py b/letta/cli/cli_load.py new file mode 100644 index 0000000..a50c525 --- /dev/null +++ b/letta/cli/cli_load.py @@ -0,0 +1,16 @@ +""" +This file contains functions for loading data into Letta's archival storage. + +Data can be loaded with the following command, once a load function is defined: +``` +letta load --name [ADDITIONAL ARGS] +``` + +""" + +import typer + +app = typer.Typer() + + +default_extensions = "txt,md,pdf" diff --git a/letta/client/__init__.py b/letta/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/letta/client/streaming.py b/letta/client/streaming.py new file mode 100644 index 0000000..9154051 --- /dev/null +++ b/letta/client/streaming.py @@ -0,0 +1,95 @@ +import json +from typing import Generator, Union, get_args + +import httpx +from httpx_sse import SSEError, connect_sse +from openai.types.chat.chat_completion_chunk import ChatCompletionChunk + +from letta.constants import OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING +from letta.errors import LLMError +from letta.log import get_logger +from letta.schemas.enums import MessageStreamStatus +from letta.schemas.letta_message import AssistantMessage, HiddenReasoningMessage, ReasoningMessage, ToolCallMessage, ToolReturnMessage +from letta.schemas.letta_response import LettaStreamingResponse +from letta.schemas.usage import LettaUsageStatistics + +logger = get_logger(__name__) + + +def _sse_post(url: str, data: dict, headers: dict) -> Generator[Union[LettaStreamingResponse, ChatCompletionChunk], None, None]: + """ + Sends an SSE POST request and yields parsed response chunks. + """ + # TODO: Please note his is a very generous timeout for e2b reasons + with httpx.Client(timeout=httpx.Timeout(5 * 60.0, read=5 * 60.0)) as client: + with connect_sse(client, method="POST", url=url, json=data, headers=headers) as event_source: + # Check for immediate HTTP errors before processing the SSE stream + if not event_source.response.is_success: + response_bytes = event_source.response.read() + logger.warning(f"SSE request error: {vars(event_source.response)}") + logger.warning(response_bytes.decode("utf-8")) + + try: + response_dict = json.loads(response_bytes.decode("utf-8")) + error_message = response_dict.get("error", {}).get("message", "") + + if OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING in error_message: + logger.error(error_message) + raise LLMError(error_message) + except LLMError: + raise + except Exception: + logger.error("Failed to parse SSE message, raising HTTP error") + event_source.response.raise_for_status() + + try: + for sse in event_source.iter_sse(): + if sse.data in {status.value for status in MessageStreamStatus}: + yield MessageStreamStatus(sse.data) + if sse.data == MessageStreamStatus.done.value: + # We received the [DONE], so stop reading the stream. + break + else: + chunk_data = json.loads(sse.data) + + if "reasoning" in chunk_data: + yield ReasoningMessage(**chunk_data) + elif chunk_data.get("message_type") == "assistant_message": + yield AssistantMessage(**chunk_data) + elif "hidden_reasoning" in chunk_data: + yield HiddenReasoningMessage(**chunk_data) + elif "tool_call" in chunk_data: + yield ToolCallMessage(**chunk_data) + elif "tool_return" in chunk_data: + yield ToolReturnMessage(**chunk_data) + elif "step_count" in chunk_data: + yield LettaUsageStatistics(**chunk_data) + elif chunk_data.get("object") == get_args(ChatCompletionChunk.__annotations__["object"])[0]: + yield ChatCompletionChunk(**chunk_data) + else: + raise ValueError(f"Unknown message type in chunk_data: {chunk_data}") + + except SSEError as e: + logger.error(f"SSE stream error: {e}") + + if "application/json" in str(e): + response = client.post(url=url, json=data, headers=headers) + + if response.headers.get("Content-Type", "").startswith("application/json"): + error_details = response.json() + logger.error(f"POST Error: {error_details}") + else: + logger.error("Failed to retrieve JSON error message via retry.") + + raise e + + except Exception as e: + logger.error(f"Unexpected exception: {e}") + + if event_source.response.request: + logger.error(f"HTTP Request: {vars(event_source.response.request)}") + if event_source.response: + logger.error(f"HTTP Status: {event_source.response.status_code}") + logger.error(f"HTTP Headers: {event_source.response.headers}") + + raise e diff --git a/letta/client/utils.py b/letta/client/utils.py new file mode 100644 index 0000000..f823ee8 --- /dev/null +++ b/letta/client/utils.py @@ -0,0 +1,78 @@ +import re +from datetime import datetime +from typing import Optional + +from IPython.display import HTML, display +from sqlalchemy.testing.plugin.plugin_base import warnings + +from letta.local_llm.constants import ASSISTANT_MESSAGE_CLI_SYMBOL, INNER_THOUGHTS_CLI_SYMBOL + + +def pprint(messages): + """Utility function for pretty-printing the output of client.send_message in notebooks""" + + css_styles = """ + + """ + + html_content = css_styles + "
" + for message in messages: + date_str = message["date"] + date_formatted = datetime.fromisoformat(date_str.replace("Z", "+00:00")).strftime("%Y-%m-%d %H:%M:%S") + + if "function_return" in message: + return_string = message["function_return"] + return_status = message["status"] + html_content += f"

🛠️ [{date_formatted}] Function Return ({return_status}):

" + html_content += f"

{return_string}

" + elif "internal_monologue" in message: + html_content += f"

{INNER_THOUGHTS_CLI_SYMBOL} [{date_formatted}] Internal Monologue:

" + html_content += f"

{message['internal_monologue']}

" + elif "function_call" in message: + html_content += f"

🛠️ [[{date_formatted}] Function Call:

" + html_content += f"

{message['function_call']}

" + elif "assistant_message" in message: + html_content += f"

{ASSISTANT_MESSAGE_CLI_SYMBOL} [{date_formatted}] Assistant Message:

" + html_content += f"

{message['assistant_message']}

" + html_content += "
" + html_content += "
" + + display(HTML(html_content)) + + +def derive_function_name_regex(function_string: str) -> Optional[str]: + # Regular expression to match the function name + match = re.search(r"def\s+([a-zA-Z_]\w*)\s*\(", function_string) + + if match: + function_name = match.group(1) + return function_name + else: + warnings.warn("No function name found.") + return None diff --git a/letta/config.py b/letta/config.py new file mode 100644 index 0000000..ed9e866 --- /dev/null +++ b/letta/config.py @@ -0,0 +1,310 @@ +import configparser +import os +from dataclasses import dataclass +from typing import Optional + +import letta +from letta.constants import ( + CORE_MEMORY_HUMAN_CHAR_LIMIT, + CORE_MEMORY_PERSONA_CHAR_LIMIT, + DEFAULT_HUMAN, + DEFAULT_PERSONA, + DEFAULT_PRESET, + LETTA_DIR, +) +from letta.log import get_logger +from letta.schemas.embedding_config import EmbeddingConfig +from letta.schemas.llm_config import LLMConfig + +logger = get_logger(__name__) + + +# helper functions for writing to configs +def get_field(config, section, field): + if section not in config: + return None + if config.has_option(section, field): + return config.get(section, field) + else: + return None + + +def set_field(config, section, field, value): + if value is None: # cannot write None + return + if section not in config: # create section + config.add_section(section) + config.set(section, field, value) + + +@dataclass +class LettaConfig: + config_path: str = os.getenv("MEMGPT_CONFIG_PATH") or os.path.join(LETTA_DIR, "config") + + # preset + preset: str = DEFAULT_PRESET # TODO: rename to system prompt + + # persona parameters + persona: str = DEFAULT_PERSONA + human: str = DEFAULT_HUMAN + + # model parameters + # default_llm_config: LLMConfig = None + + # embedding parameters + # default_embedding_config: EmbeddingConfig = None + + # NONE OF THIS IS CONFIG ↓↓↓↓↓ + # @norton120 these are the metdadatastore + + # database configs: archival + archival_storage_type: str = "sqlite" # local, db + archival_storage_path: str = LETTA_DIR + archival_storage_uri: str = None # TODO: eventually allow external vector DB + + # database configs: recall + recall_storage_type: str = "sqlite" # local, db + recall_storage_path: str = LETTA_DIR + recall_storage_uri: str = None # TODO: eventually allow external vector DB + + # database configs: metadata storage (sources, agents, data sources) + metadata_storage_type: str = "sqlite" + metadata_storage_path: str = LETTA_DIR + metadata_storage_uri: str = None + + # database configs: agent state + persistence_manager_type: str = None # in-memory, db + persistence_manager_save_file: str = None # local file + persistence_manager_uri: str = None # db URI + + # version (for backcompat) + letta_version: str = letta.__version__ + + # user info + policies_accepted: bool = False + + # Default memory limits + core_memory_persona_char_limit: int = CORE_MEMORY_PERSONA_CHAR_LIMIT + core_memory_human_char_limit: int = CORE_MEMORY_HUMAN_CHAR_LIMIT + + def __post_init__(self): + # ensure types + # self.embedding_chunk_size = int(self.embedding_chunk_size) + # self.embedding_dim = int(self.embedding_dim) + # self.context_window = int(self.context_window) + pass + + @classmethod + def load(cls, llm_config: Optional[LLMConfig] = None, embedding_config: Optional[EmbeddingConfig] = None) -> "LettaConfig": + # avoid circular import + from letta.utils import printd + + # from letta.migrate import VERSION_CUTOFF, config_is_compatible + # if not config_is_compatible(allow_empty=True): + # error_message = " ".join( + # [ + # f"\nYour current config file is incompatible with Letta versions later than {VERSION_CUTOFF}.", + # f"\nTo use Letta, you must either downgrade your Letta version (<= {VERSION_CUTOFF}) or regenerate your config using `letta configure`, or `letta migrate` if you would like to migrate old agents.", + # ] + # ) + # raise ValueError(error_message) + + config = configparser.ConfigParser() + + # allow overriding with env variables + if os.getenv("MEMGPT_CONFIG_PATH"): + config_path = os.getenv("MEMGPT_CONFIG_PATH") + else: + config_path = LettaConfig.config_path + + # insure all configuration directories exist + cls.create_config_dir() + printd(f"Loading config from {config_path}") + if os.path.exists(config_path): + # read existing config + config.read(config_path) + + ## Handle extraction of nested LLMConfig and EmbeddingConfig + # llm_config_dict = { + # # Extract relevant LLM configuration from the config file + # "model": get_field(config, "model", "model"), + # "model_endpoint": get_field(config, "model", "model_endpoint"), + # "model_endpoint_type": get_field(config, "model", "model_endpoint_type"), + # "model_wrapper": get_field(config, "model", "model_wrapper"), + # "context_window": get_field(config, "model", "context_window"), + # } + # embedding_config_dict = { + # # Extract relevant Embedding configuration from the config file + # "embedding_endpoint": get_field(config, "embedding", "embedding_endpoint"), + # "embedding_model": get_field(config, "embedding", "embedding_model"), + # "embedding_endpoint_type": get_field(config, "embedding", "embedding_endpoint_type"), + # "embedding_dim": get_field(config, "embedding", "embedding_dim"), + # "embedding_chunk_size": get_field(config, "embedding", "embedding_chunk_size"), + # } + ## Remove null values + # llm_config_dict = {k: v for k, v in llm_config_dict.items() if v is not None} + # embedding_config_dict = {k: v for k, v in embedding_config_dict.items() if v is not None} + # Correct the types that aren't strings + # if "context_window" in llm_config_dict and llm_config_dict["context_window"] is not None: + # llm_config_dict["context_window"] = int(llm_config_dict["context_window"]) + # if "embedding_dim" in embedding_config_dict and embedding_config_dict["embedding_dim"] is not None: + # embedding_config_dict["embedding_dim"] = int(embedding_config_dict["embedding_dim"]) + # if "embedding_chunk_size" in embedding_config_dict and embedding_config_dict["embedding_chunk_size"] is not None: + # embedding_config_dict["embedding_chunk_size"] = int(embedding_config_dict["embedding_chunk_size"]) + ## Construct the inner properties + # llm_config = LLMConfig(**llm_config_dict) + # embedding_config = EmbeddingConfig(**embedding_config_dict) + + # Everything else + config_dict = { + # Two prepared configs + # "default_llm_config": llm_config, + # "default_embedding_config": embedding_config, + # Agent related + "preset": get_field(config, "defaults", "preset"), + "persona": get_field(config, "defaults", "persona"), + "human": get_field(config, "defaults", "human"), + "agent": get_field(config, "defaults", "agent"), + # Storage related + "archival_storage_type": get_field(config, "archival_storage", "type"), + "archival_storage_path": get_field(config, "archival_storage", "path"), + "archival_storage_uri": get_field(config, "archival_storage", "uri"), + "recall_storage_type": get_field(config, "recall_storage", "type"), + "recall_storage_path": get_field(config, "recall_storage", "path"), + "recall_storage_uri": get_field(config, "recall_storage", "uri"), + "metadata_storage_type": get_field(config, "metadata_storage", "type"), + "metadata_storage_path": get_field(config, "metadata_storage", "path"), + "metadata_storage_uri": get_field(config, "metadata_storage", "uri"), + # Misc + "config_path": config_path, + "letta_version": get_field(config, "version", "letta_version"), + } + # Don't include null values + config_dict = {k: v for k, v in config_dict.items() if v is not None} + + return cls(**config_dict) + + # assert embedding_config is not None, "Embedding config must be provided if config does not exist" + # assert llm_config is not None, "LLM config must be provided if config does not exist" + + # create new config + config = cls(config_path=config_path) + + config.create_config_dir() # create dirs + + return config + + def save(self): + import letta + + config = configparser.ConfigParser() + + # CLI defaults + set_field(config, "defaults", "preset", self.preset) + set_field(config, "defaults", "persona", self.persona) + set_field(config, "defaults", "human", self.human) + + # model defaults + # set_field(config, "model", "model", self.default_llm_config.model) + ##set_field(config, "model", "model_endpoint", self.default_llm_config.model_endpoint) + # set_field( + # config, + # "model", + # "model_endpoint_type", + # self.default_llm_config.model_endpoint_type, + # ) + # set_field(config, "model", "model_wrapper", self.default_llm_config.model_wrapper) + # set_field( + # config, + # "model", + # "context_window", + # str(self.default_llm_config.context_window), + # ) + + ## embeddings + # set_field( + # config, + # "embedding", + # "embedding_endpoint_type", + # self.default_embedding_config.embedding_endpoint_type, + # ) + # set_field( + # config, + # "embedding", + # "embedding_endpoint", + # self.default_embedding_config.embedding_endpoint, + # ) + # set_field( + # config, + # "embedding", + # "embedding_model", + # self.default_embedding_config.embedding_model, + # ) + # set_field( + # config, + # "embedding", + # "embedding_dim", + # str(self.default_embedding_config.embedding_dim), + # ) + # set_field( + # config, + # "embedding", + # "embedding_chunk_size", + # str(self.default_embedding_config.embedding_chunk_size), + # ) + + # archival storage + set_field(config, "archival_storage", "type", self.archival_storage_type) + set_field(config, "archival_storage", "path", self.archival_storage_path) + set_field(config, "archival_storage", "uri", self.archival_storage_uri) + + # recall storage + set_field(config, "recall_storage", "type", self.recall_storage_type) + set_field(config, "recall_storage", "path", self.recall_storage_path) + set_field(config, "recall_storage", "uri", self.recall_storage_uri) + + # metadata storage + set_field(config, "metadata_storage", "type", self.metadata_storage_type) + set_field(config, "metadata_storage", "path", self.metadata_storage_path) + set_field(config, "metadata_storage", "uri", self.metadata_storage_uri) + + # set version + set_field(config, "version", "letta_version", letta.__version__) + + # always make sure all directories are present + self.create_config_dir() + + with open(self.config_path, "w", encoding="utf-8") as f: + config.write(f) + logger.debug(f"Saved Config: {self.config_path}") + + @staticmethod + def exists(): + # allow overriding with env variables + if os.getenv("MEMGPT_CONFIG_PATH"): + config_path = os.getenv("MEMGPT_CONFIG_PATH") + else: + config_path = LettaConfig.config_path + + assert not os.path.isdir(config_path), f"Config path {config_path} cannot be set to a directory." + return os.path.exists(config_path) + + @staticmethod + def create_config_dir(): + if not os.path.exists(LETTA_DIR): + os.makedirs(LETTA_DIR, exist_ok=True) + + folders = [ + "personas", + "humans", + "archival", + "agents", + "functions", + "system_prompts", + "presets", + "settings", + ] + + for folder in folders: + if not os.path.exists(os.path.join(LETTA_DIR, folder)): + os.makedirs(os.path.join(LETTA_DIR, folder)) diff --git a/letta/config_file.py b/letta/config_file.py new file mode 100644 index 0000000..1b6116d --- /dev/null +++ b/letta/config_file.py @@ -0,0 +1,232 @@ +""" +Letta Configuration File Support + +Loads hierarchical YAML config and maps it to environment variables. + +Supported top-level keys and their env var prefixes: + letta: -> LETTA_* + model: -> * (provider-prefixed: OPENAI_*, ANTHROPIC_*, etc.) + tool: -> * (prefix-based: E2B_*, MCP_*, TOOL_*, etc.) + datadog: -> DD_* + +Config file format: + letta: + telemetry: + enable_datadog: true + pg: + host: localhost + model: + openai: + api_key: sk-xxx + anthropic: + api_key: sk-yyy + tool: + e2b: + api_key: xxx + mcp: + disable_stdio: true + datadog: + site: us5.datadoghq.com + service: memgpt-server + +This maps to environment variables: + LETTA_TELEMETRY_ENABLE_DATADOG=true + LETTA_PG_HOST=localhost + OPENAI_API_KEY=sk-xxx + ANTHROPIC_API_KEY=sk-yyy + E2B_API_KEY=xxx + MCP_DISABLE_STDIO=true + DD_SITE=us5.datadoghq.com + DD_SERVICE=memgpt-server + +Config file locations (in order of precedence): + 1. ~/.letta/conf.yaml + 2. ./conf.yaml + 3. LETTA_CONFIG_PATH environment variable +""" + +import os +from pathlib import Path +from typing import Any + +import yaml + +# Config file locations +DEFAULT_USER_CONFIG = Path.home() / ".letta" / "conf.yaml" +DEFAULT_PROJECT_CONFIG = Path.cwd() / "conf.yaml" + + +def load_config_file(config_path: str | Path | None = None) -> dict[str, Any]: + """ + Load configuration from YAML file. + + Args: + config_path: Optional explicit path to config file + + Returns: + Loaded config dict, or empty dict if no config found + """ + paths_to_check = [] + + # Check in order of precedence (lowest to highest) + if DEFAULT_USER_CONFIG.exists(): + paths_to_check.append(DEFAULT_USER_CONFIG) + + if DEFAULT_PROJECT_CONFIG.exists(): + paths_to_check.append(DEFAULT_PROJECT_CONFIG) + + # Environment variable override + env_path = os.environ.get("LETTA_CONFIG_PATH") + if env_path and Path(env_path).exists(): + paths_to_check.append(Path(env_path)) + + # Explicit path has highest precedence + if config_path: + p = Path(config_path) + if p.exists(): + paths_to_check.append(p) + + # Merge configs (later files override earlier) + config: dict[str, Any] = {} + for path in paths_to_check: + try: + with open(path, "r") as f: + file_config = yaml.safe_load(f) + if file_config: + config = _deep_merge(config, file_config) + except Exception: + pass + + return config + + +def _deep_merge(base: dict, override: dict) -> dict: + """Deep merge two dicts, override values take precedence.""" + result = base.copy() + for key, value in override.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = _deep_merge(result[key], value) + else: + result[key] = value + return result + + +def _flatten_with_prefix(d: dict, prefix: str, env_vars: dict[str, str]) -> None: + """Flatten a dict with a given prefix.""" + for key, value in d.items(): + env_key = f"{prefix}_{key}".upper() if prefix else key.upper() + if isinstance(value, dict): + _flatten_with_prefix(value, env_key, env_vars) + elif value is not None: + if isinstance(value, bool): + env_vars[env_key] = str(value).lower() + else: + env_vars[env_key] = str(value) + + +def _flatten_model_settings(d: dict, env_vars: dict[str, str]) -> None: + """ + Flatten model settings where nested keys become prefixes. + + model: + openai: + api_key: xxx -> OPENAI_API_KEY + api_base: yyy -> OPENAI_API_BASE + anthropic: + api_key: zzz -> ANTHROPIC_API_KEY + global_max_context_window_limit: 128000 -> GLOBAL_MAX_CONTEXT_WINDOW_LIMIT + """ + for key, value in d.items(): + if isinstance(value, dict): + # Nested provider config: openai.api_key -> OPENAI_API_KEY + _flatten_with_prefix(value, key.upper(), env_vars) + elif value is not None: + # Top-level model setting + env_key = key.upper() + if isinstance(value, bool): + env_vars[env_key] = str(value).lower() + else: + env_vars[env_key] = str(value) + + +def _flatten_tool_settings(d: dict, env_vars: dict[str, str]) -> None: + """ + Flatten tool settings where nested keys become prefixes. + + tool: + e2b: + api_key: xxx -> E2B_API_KEY + sandbox_template_id: y -> E2B_SANDBOX_TEMPLATE_ID + mcp: + disable_stdio: true -> MCP_DISABLE_STDIO + tool_sandbox_timeout: 180 -> TOOL_SANDBOX_TIMEOUT + """ + for key, value in d.items(): + if isinstance(value, dict): + # Nested tool config: e2b.api_key -> E2B_API_KEY + _flatten_with_prefix(value, key.upper(), env_vars) + elif value is not None: + # Top-level tool setting + env_key = key.upper() + if isinstance(value, bool): + env_vars[env_key] = str(value).lower() + else: + env_vars[env_key] = str(value) + + +def config_to_env_vars(config: dict[str, Any]) -> dict[str, str]: + """ + Convert hierarchical config to flat environment variables. + + Supports multiple top-level keys with different prefix behaviors: + - letta: -> LETTA_* prefix + - model: -> provider-prefixed (OPENAI_*, ANTHROPIC_*, etc.) + - tool: -> prefix-based (E2B_*, MCP_*, TOOL_*, etc.) + - datadog: -> DD_* prefix + + Args: + config: Hierarchical config dict + + Returns: + Dict of environment variable name -> value + """ + env_vars: dict[str, str] = {} + + # Handle 'letta' section with LETTA_ prefix + if "letta" in config: + _flatten_with_prefix(config["letta"], "LETTA", env_vars) + + # Handle 'model' section (provider-prefixed env vars) + if "model" in config: + _flatten_model_settings(config["model"], env_vars) + + # Handle 'tool' section (prefix-based env vars) + if "tool" in config: + _flatten_tool_settings(config["tool"], env_vars) + + # Handle 'datadog' section with DD_ prefix + if "datadog" in config: + _flatten_with_prefix(config["datadog"], "DD", env_vars) + + return env_vars + + +def apply_config_to_env(config_path: str | Path | None = None) -> None: + """ + Load config file and apply values to environment variables. + + Environment variables already set take precedence over config file values. + + Args: + config_path: Optional explicit path to config file + """ + config = load_config_file(config_path) + if not config: + return + + env_vars = config_to_env_vars(config) + + for key, value in env_vars.items(): + # Only set if not already in environment (env vars take precedence) + if key not in os.environ: + os.environ[key] = value diff --git a/letta/constants.py b/letta/constants.py new file mode 100644 index 0000000..c79eac7 --- /dev/null +++ b/letta/constants.py @@ -0,0 +1,538 @@ +import os +import re +from logging import CRITICAL, DEBUG, ERROR, INFO, NOTSET, WARN, WARNING + +LETTA_DIR = os.path.join(os.path.expanduser("~"), ".letta") +LETTA_TOOL_EXECUTION_DIR = os.path.join(LETTA_DIR, "tool_execution_dir") + +LETTA_MODEL_ENDPOINT = "https://inference.letta.com/v1/" +DEFAULT_TIMEZONE = "UTC" + +# Provider ordering for model listing (matches original _enabled_providers list order) +PROVIDER_ORDER = { + "letta": 0, + "openai": 1, + "anthropic": 2, + "ollama": 3, + "google_ai": 4, + "google_vertex": 5, + "azure": 6, + "groq": 7, + "together": 8, + "vllm": 9, + "bedrock": 10, + "deepseek": 11, + "xai": 12, + "lmstudio": 13, + "zai": 14, + "zai_coding": 15, + "openrouter": 16, +} + +ADMIN_PREFIX = "/v1/admin" +API_PREFIX = "/v1" +OLLAMA_API_PREFIX = "/v1" +OPENAI_API_PREFIX = "/openai" + +MCP_CONFIG_NAME = "mcp_config.json" +MCP_TOOL_TAG_NAME_PREFIX = "mcp" # full format, mcp:server_name +SUBAGENT_ROLE_TAG = "role:subagent" + +LETTA_CORE_TOOL_MODULE_NAME = "letta.functions.function_sets.base" +LETTA_MULTI_AGENT_TOOL_MODULE_NAME = "letta.functions.function_sets.multi_agent" +LETTA_VOICE_TOOL_MODULE_NAME = "letta.functions.function_sets.voice" +LETTA_BUILTIN_TOOL_MODULE_NAME = "letta.functions.function_sets.builtin" +LETTA_FILES_TOOL_MODULE_NAME = "letta.functions.function_sets.files" + +LETTA_TOOL_MODULE_NAMES = [ + LETTA_CORE_TOOL_MODULE_NAME, + LETTA_MULTI_AGENT_TOOL_MODULE_NAME, + LETTA_VOICE_TOOL_MODULE_NAME, + LETTA_BUILTIN_TOOL_MODULE_NAME, + LETTA_FILES_TOOL_MODULE_NAME, +] + +DEFAULT_ORG_ID = "org-00000000-0000-4000-8000-000000000000" +DEFAULT_ORG_NAME = "default_org" + +# String in the error message for when the context window is too large +# Example full message: +# This model's maximum context length is 8192 tokens. However, your messages resulted in 8198 tokens (7450 in the messages, 748 in the functions). Please reduce the length of the messages or functions. +OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING = "maximum context length" + +# System prompt templating +IN_CONTEXT_MEMORY_KEYWORD = "CORE_MEMORY" + +# OpenAI error message: Invalid 'messages[1].tool_calls[0].id': string too long. Expected a string with maximum length 29, but got a string with length 36 instead. +TOOL_CALL_ID_MAX_LEN = 29 + +# Maximum length for tool names to support Modal deployment +# Modal function names are limited to 64 characters: tool_name + "_" + project_id +# Reserving 16 characters for project_id suffix (e.g., "_project-12345678") +MAX_TOOL_NAME_LENGTH = 48 + +# Max steps for agent loop +DEFAULT_MAX_STEPS = 50 + +# context window size +MIN_CONTEXT_WINDOW = 4096 +DEFAULT_CONTEXT_WINDOW = 128000 + +# Summarization trigger threshold (multiplier of context_window limit) +# Summarization triggers when step usage > context_window * SUMMARIZATION_TRIGGER_MULTIPLIER +SUMMARIZATION_TRIGGER_MULTIPLIER = 0.9 # using instead of 1.0 to avoid "too many tokens in prompt" fallbacks + +# number of concurrent embedding requests to sent +EMBEDDING_BATCH_SIZE = 200 + +# Voice Sleeptime message buffer lengths +DEFAULT_MAX_MESSAGE_BUFFER_LENGTH = 30 +DEFAULT_MIN_MESSAGE_BUFFER_LENGTH = 15 + +# embeddings +MAX_EMBEDDING_DIM = 4096 # maximum supported embeding size - do NOT change or else DBs will need to be reset +DEFAULT_EMBEDDING_CHUNK_SIZE = 300 +DEFAULT_EMBEDDING_DIM = 1024 + +# tokenizers +EMBEDDING_TO_TOKENIZER_MAP = { + "text-embedding-3-small": "cl100k_base", +} +EMBEDDING_TO_TOKENIZER_DEFAULT = "cl100k_base" + + +DEFAULT_LETTA_MODEL = "gpt-4" # TODO: fixme +DEFAULT_PERSONA = "sam_pov" +DEFAULT_HUMAN = "basic" +DEFAULT_PRESET = "memgpt_chat" + +DEFAULT_PERSONA_BLOCK_DESCRIPTION = "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." +DEFAULT_HUMAN_BLOCK_DESCRIPTION = "The human block: Stores key details about the person you are conversing with, allowing for more personalized and friend-like conversation." + +SEND_MESSAGE_TOOL_NAME = "send_message" +# Base tools that cannot be edited, as they access agent state directly +# Note that we don't include "conversation_search_date" for now +BASE_TOOLS = [SEND_MESSAGE_TOOL_NAME, "conversation_search", "archival_memory_insert", "archival_memory_search"] +DEPRECATED_LETTA_TOOLS = ["archival_memory_insert", "archival_memory_search"] +# Base memory tools CAN be edited, and are added by default by the server +BASE_MEMORY_TOOLS = ["core_memory_append", "core_memory_replace", "memory", "memory_apply_patch"] +# New v2 collection of the base memory tools (effecitvely same as sleeptime set), to pair with memgpt_v2 prompt +BASE_MEMORY_TOOLS_V2 = [ + "memory_replace", + "memory_insert", + # NOTE: leaving these ones out to simply the set? Can have these reserved for sleep-time + # "memory_rethink", + # "memory_finish_edits", +] + +# v3 collection, currently just a omni memory tool for anthropic +BASE_MEMORY_TOOLS_V3 = [ + "memory", +] +# Base tools if the memgpt agent has enable_sleeptime on +BASE_SLEEPTIME_CHAT_TOOLS = [SEND_MESSAGE_TOOL_NAME, "conversation_search", "archival_memory_search"] +# Base memory tools for sleeptime agent +BASE_SLEEPTIME_TOOLS = [ + "memory_replace", + "memory_insert", + "memory_rethink", + "memory_finish_edits", + # "archival_memory_insert", + # "archival_memory_search", + # "conversation_search", +] +# Base tools for the voice agent +BASE_VOICE_SLEEPTIME_CHAT_TOOLS = [SEND_MESSAGE_TOOL_NAME, "search_memory"] +# Base memory tools for sleeptime agent +BASE_VOICE_SLEEPTIME_TOOLS = [ + "store_memories", + "rethink_user_memory", + "finish_rethinking_memory", +] + +# Multi agent tools +MULTI_AGENT_TOOLS = ["send_message_to_agent_and_wait_for_reply", "send_message_to_agents_matching_tags", "send_message_to_agent_async"] +LOCAL_ONLY_MULTI_AGENT_TOOLS = ["send_message_to_agent_async"] + +# Used to catch if line numbers are pushed in +# MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX = re.compile(r"^Line \d+: ", re.MULTILINE) +# Updated to match new arrow format: "1→ content" +# shared constant for both memory_insert and memory_replace +MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX = re.compile( + r"^[ \t]*\d+→[ \t]*", # match number followed by arrow, with optional whitespace + re.MULTILINE, +) + +# Built in tools +BUILTIN_TOOLS = ["run_code", "run_code_with_tools", "web_search", "fetch_webpage"] + +# Built in tools +FILES_TOOLS = ["open_files", "grep_files", "semantic_search_files"] + +FILE_MEMORY_EXISTS_MESSAGE = "The following files are currently accessible in memory:" +FILE_MEMORY_EMPTY_MESSAGE = ( + "There are no files currently available in memory. Files will appear here once they are uploaded directly to your system." +) + +# Set of all built-in Letta tools +LETTA_TOOL_SET = set( + BASE_TOOLS + + BASE_MEMORY_TOOLS + + MULTI_AGENT_TOOLS + + BASE_SLEEPTIME_TOOLS + + BASE_VOICE_SLEEPTIME_TOOLS + + BASE_VOICE_SLEEPTIME_CHAT_TOOLS + + BUILTIN_TOOLS + + FILES_TOOLS +) + +LETTA_PARALLEL_SAFE_TOOLS = { + "conversation_search", + "archival_memory_search", + "run_code", + "web_search", + "fetch_webpage", + "grep_files", + "semantic_search_files", +} + + +def FUNCTION_RETURN_VALUE_TRUNCATED(return_str, return_char: int, return_char_limit: int): + return ( + f"{return_str}... [NOTE: function output was truncated since it exceeded the character limit: {return_char} > {return_char_limit}]" + ) + + +# The name of the tool used to send message to the user +# May not be relevant in cases where the agent has multiple ways to message to user (send_imessage, send_discord_mesasge, ...) +# or in cases where the agent has no concept of messaging a user (e.g. a workflow agent) +DEFAULT_MESSAGE_TOOL = SEND_MESSAGE_TOOL_NAME +DEFAULT_MESSAGE_TOOL_KWARG = "message" + +# The name of the conversation search tool - messages with this tool should not be indexed +CONVERSATION_SEARCH_TOOL_NAME = "conversation_search" + +PRE_EXECUTION_MESSAGE_ARG = "pre_exec_msg" + +REQUEST_HEARTBEAT_PARAM = "request_heartbeat" +REQUEST_HEARTBEAT_DESCRIPTION = "Request an immediate heartbeat after function execution. You MUST set this value to `True` if you want to send a follow-up message or run a follow-up tool call (chain multiple tools together). If set to `False` (the default), then the chain of execution will end immediately after this function call." + +# Automated tool call denials +TOOL_CALL_DENIAL_ON_CANCEL = "The user cancelled the request, so the tool call was denied." + +# Structured output models +STRUCTURED_OUTPUT_MODELS = {"gpt-4o", "gpt-4o-mini"} + +# LOGGER_LOG_LEVEL is use to convert Text to Logging level value for logging mostly for Cli input to setting level +LOGGER_LOG_LEVELS = {"CRITICAL": CRITICAL, "ERROR": ERROR, "WARN": WARN, "WARNING": WARNING, "INFO": INFO, "DEBUG": DEBUG, "NOTSET": NOTSET} + +FIRST_MESSAGE_ATTEMPTS = 10 + +INITIAL_BOOT_MESSAGE = "Boot sequence complete. Persona activated." +INITIAL_BOOT_MESSAGE_SEND_MESSAGE_THOUGHT = "Bootup sequence complete. Persona activated. Testing messaging functionality." +STARTUP_QUOTES = [ + "I think, therefore I am.", + "All those moments will be lost in time, like tears in rain.", + "More human than human is our motto.", +] +INITIAL_BOOT_MESSAGE_SEND_MESSAGE_FIRST_MSG = STARTUP_QUOTES[2] + +CLI_WARNING_PREFIX = "Warning: " + +ERROR_MESSAGE_PREFIX = "Error" + +NON_USER_MSG_PREFIX = "[This is an automated system message hidden from the user] " + +CORE_MEMORY_LINE_NUMBER_WARNING = "# NOTE: Line numbers shown below (with arrows like '1→') are to help during editing. Do NOT include line number prefixes in your memory edit tool calls." + + +# Constants to do with summarization / conversation length window +# The max amount of tokens supported by the underlying model (eg 8k for gpt-4 and Mistral 7B) +LLM_MAX_CONTEXT_WINDOW = { + "DEFAULT": 30000, + # deepseek + "deepseek-chat": 64000, + "deepseek-reasoner": 64000, + # glm (Z.AI) + "glm-4.5": 128000, + "glm-4.6": 180000, + "glm-4.7": 180000, + "glm-5": 180000, + "glm-5-code": 180000, + # kimi (moonshot) + "kimi-k2.5": 262144, + "kimi-k2-thinking": 256000, + "kimi-k2-0905": 262144, + ## OpenAI models: https://platform.openai.com/docs/models/overview + # gpt-5 + "gpt-5": 272000, + "gpt-5-2025-08-07": 272000, + "gpt-5-mini": 272000, + "gpt-5-mini-2025-08-07": 272000, + "gpt-5-nano": 272000, + "gpt-5-nano-2025-08-07": 272000, + "gpt-5-codex": 272000, + # gpt-5.1 + "gpt-5.1": 272000, + "gpt-5.1-2025-11-13": 272000, + "gpt-5.1-codex": 272000, + "gpt-5.1-codex-mini": 272000, + "gpt-5.1-codex-max": 272000, + # gpt-5.2 + "gpt-5.2": 272000, + "gpt-5.2-2025-12-11": 272000, + "gpt-5.2-pro": 272000, + "gpt-5.2-pro-2025-12-11": 272000, + "gpt-5.2-codex": 272000, + # gpt-5.3 + "gpt-5.3-codex": 272000, + # gpt-5.4 + "gpt-5.4": 1050000, + "gpt-5.4-fast": 1050000, + "gpt-5.4-2026-03-05": 1050000, + "gpt-5.4-mini": 400000, + "gpt-5.4-nano": 400000, + # reasoners + "o1": 200000, + # "o1-pro": 200000, # responses API only + "o1-2024-12-17": 200000, + "o3": 200000, + "o3-2025-04-16": 200000, + "o3-mini": 200000, + "o3-mini-2025-01-31": 200000, + # "o3-pro": 200000, # responses API only + # "o3-pro-2025-06-10": 200000, + "gpt-4.1": 1047576, + "gpt-4.1-2025-04-14": 1047576, + "gpt-4.1-mini": 1047576, + "gpt-4.1-mini-2025-04-14": 1047576, + "gpt-4.1-nano": 1047576, + "gpt-4.1-nano-2025-04-14": 1047576, + # gpt-4.5-preview + "gpt-4.5-preview": 128000, + "gpt-4.5-preview-2025-02-27": 128000, + # "o1-preview + "chatgpt-4o-latest": 128000, + # "o1-preview-2024-09-12 + "gpt-4o-2024-08-06": 128000, + "gpt-4o-2024-11-20": 128000, + "gpt-4-turbo-preview": 128000, + "gpt-4o": 128000, + "gpt-3.5-turbo-instruct": 16385, + "gpt-4-0125-preview": 128000, + "gpt-3.5-turbo-0125": 16385, + # "babbage-002": 128000, + # "davinci-002": 128000, + "gpt-4-turbo-2024-04-09": 128000, + # "gpt-4o-realtime-preview-2024-10-01 + "gpt-4-turbo": 128000, + "gpt-4o-2024-05-13": 128000, + # "o1-mini + # "o1-mini-2024-09-12 + # "gpt-3.5-turbo-instruct-0914 + "gpt-4o-mini": 128000, + # "gpt-4o-realtime-preview + "gpt-4o-mini-2024-07-18": 128000, + # gpt-4 + "gpt-4-1106-preview": 128000, + "gpt-4": 8192, + "gpt-4-32k": 32768, + "gpt-4-0613": 8192, + "gpt-4-32k-0613": 32768, + "gpt-4-0314": 8192, # legacy + "gpt-4-32k-0314": 32768, # legacy + # gpt-3.5 + "gpt-3.5-turbo-1106": 16385, + "gpt-3.5-turbo": 4096, + "gpt-3.5-turbo-16k": 16385, + "gpt-3.5-turbo-0613": 4096, # legacy + "gpt-3.5-turbo-16k-0613": 16385, # legacy + "gpt-3.5-turbo-0301": 4096, # legacy + "gemini-1.0-pro-vision-latest": 12288, + "gemini-pro-vision": 12288, + "gemini-1.5-pro-latest": 2000000, + "gemini-1.5-pro-001": 2000000, + "gemini-1.5-pro-002": 2000000, + "gemini-1.5-pro": 2000000, + "gemini-1.5-flash-latest": 1000000, + "gemini-1.5-flash-001": 1000000, + "gemini-1.5-flash-001-tuning": 16384, + "gemini-1.5-flash": 1000000, + "gemini-1.5-flash-002": 1000000, + "gemini-1.5-flash-8b": 1000000, + "gemini-1.5-flash-8b-001": 1000000, + "gemini-1.5-flash-8b-latest": 1000000, + "gemini-1.5-flash-8b-exp-0827": 1000000, + "gemini-1.5-flash-8b-exp-0924": 1000000, + "gemini-2.5-pro-exp-03-25": 1048576, + "gemini-2.5-pro-preview-03-25": 1048576, + "gemini-2.5-flash-preview-04-17": 1048576, + "gemini-2.5-flash-preview-05-20": 1048576, + "gemini-2.5-flash-preview-04-17-thinking": 1048576, + "gemini-2.5-pro-preview-05-06": 1048576, + "gemini-2.0-flash-exp": 1048576, + "gemini-2.0-flash": 1048576, + "gemini-2.0-flash-001": 1048576, + "gemini-2.0-flash-exp-image-generation": 1048576, + "gemini-2.0-flash-lite-001": 1048576, + "gemini-2.0-flash-lite": 1048576, + "gemini-2.0-flash-preview-image-generation": 32768, + "gemini-2.0-flash-lite-preview-02-05": 1048576, + "gemini-2.0-flash-lite-preview": 1048576, + "gemini-2.0-pro-exp": 1048576, + "gemini-2.0-pro-exp-02-05": 1048576, + "gemini-exp-1206": 1048576, + "gemini-2.0-flash-thinking-exp-01-21": 1048576, + "gemini-2.0-flash-thinking-exp": 1048576, + "gemini-2.0-flash-thinking-exp-1219": 1048576, + "gemini-2.5-flash-preview-tts": 32768, + "gemini-2.5-pro-preview-tts": 65536, + # gemini 2.5 stable releases + "gemini-2.5-flash": 1048576, + "gemini-2.5-flash-lite": 1048576, + "gemini-2.5-pro": 1048576, + "gemini-2.5-pro-preview-06-05": 1048576, + "gemini-2.5-flash-lite-preview-06-17": 1048576, + "gemini-2.5-flash-image": 1048576, + "gemini-2.5-flash-image-preview": 1048576, + "gemini-2.5-flash-preview-09-2025": 1048576, + "gemini-2.5-flash-lite-preview-09-2025": 1048576, + "gemini-2.5-computer-use-preview-10-2025": 1048576, + # gemini 3 + "gemini-3.1-pro-preview": 1048576, + "gemini-3-flash-preview": 1048576, + # gemini latest aliases + "gemini-flash-latest": 1048576, + "gemini-flash-lite-latest": 1048576, + "gemini-pro-latest": 1048576, + # gemini specialized models + "gemini-robotics-er-1.5-preview": 1048576, +} +# The error message that Letta will receive +# MESSAGE_SUMMARY_WARNING_STR = f"Warning: the conversation history will soon reach its maximum length and be trimmed. Make sure to save any important information from the conversation to your memory before it is removed." +# Much longer and more specific variant of the prompt +MESSAGE_SUMMARY_WARNING_STR = " ".join( + [ + f"{NON_USER_MSG_PREFIX}The conversation history will soon reach its maximum length and be trimmed.", + "Do NOT tell the user about this system alert, they should not know that the history is reaching max length.", + "If there is any important new information or general memories about you or the user that you would like to save, you should save that information immediately by calling function core_memory_append, core_memory_replace, or archival_memory_insert.", + # "Remember to pass request_heartbeat = true if you would like to send a message immediately after.", + ] +) + +# Throw an error message when a read-only block is edited +READ_ONLY_BLOCK_EDIT_ERROR = f"{ERROR_MESSAGE_PREFIX} This block is read-only and cannot be edited." + +# The ackknowledgement message used in the summarize sequence +MESSAGE_SUMMARY_REQUEST_ACK = "Understood, I will respond with a summary of the message (and only the summary, nothing else) once I receive the conversation history. I'm ready." + +# Maximum length of an error message +MAX_ERROR_MESSAGE_CHAR_LIMIT = 1000 + +# Default memory limits +CORE_MEMORY_PERSONA_CHAR_LIMIT: int = 20000 +CORE_MEMORY_HUMAN_CHAR_LIMIT: int = 20000 +CORE_MEMORY_BLOCK_CHAR_LIMIT: int = 100000 + +# Function return limits +FUNCTION_RETURN_CHAR_LIMIT = 50000 # ~300 words +BASE_FUNCTION_RETURN_CHAR_LIMIT = 50000 # same as regular function limit +FILE_IS_TRUNCATED_WARNING = "# NOTE: This block is truncated, use functions to view the full content." + +# Tool return truncation limit for LLM context window management +TOOL_RETURN_TRUNCATION_CHARS = 5000 + +MAX_PAUSE_HEARTBEATS = 360 # in min + +MESSAGE_CHATGPT_FUNCTION_MODEL = "gpt-3.5-turbo" +MESSAGE_CHATGPT_FUNCTION_SYSTEM_MESSAGE = "You are a helpful assistant. Keep your responses short and concise." + +#### Functions related + +# REQ_HEARTBEAT_MESSAGE = f"{NON_USER_MSG_PREFIX}request_heartbeat == true" +REQ_HEARTBEAT_MESSAGE = f"{NON_USER_MSG_PREFIX}Function called using request_heartbeat=true, returning control" +# FUNC_FAILED_HEARTBEAT_MESSAGE = f"{NON_USER_MSG_PREFIX}Function call failed" +FUNC_FAILED_HEARTBEAT_MESSAGE = f"{NON_USER_MSG_PREFIX}Function call failed, returning control" + + +RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE = 5 + +MAX_FILENAME_LENGTH = 255 +RESERVED_FILENAMES = {"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "LPT1", "LPT2"} + +WEB_SEARCH_CLIP_CONTENT = False +WEB_SEARCH_INCLUDE_SCORE = False +WEB_SEARCH_SEPARATOR = "\n" + "-" * 40 + "\n" + +REDIS_INCLUDE = "include" +REDIS_EXCLUDE = "exclude" +REDIS_SET_DEFAULT_VAL = "None" +REDIS_DEFAULT_CACHE_PREFIX = "letta_cache" +REDIS_RUN_ID_PREFIX = "agent:send_message:run_id" + +# Conversation lock constants +CONVERSATION_LOCK_PREFIX = "conversation:lock:" +CONVERSATION_LOCK_TTL_SECONDS = 300 # 5 minutes + +# OTID -> run_id mapping (for recovering from duplicate requests) +OTID_RUN_PREFIX = "otid:run:" +OTID_RUN_TTL_SECONDS = 10800 # 3 hours (same as stream TTL) + +# Memory repo locks - prevents concurrent modifications to git-based memory +MEMORY_REPO_LOCK_PREFIX = "memory_repo:lock:" +MEMORY_REPO_LOCK_TTL_SECONDS = 60 # 1 minute (git operations should be fast) + +# TODO: This is temporary, eventually use token-based eviction +# File based controls +DEFAULT_MAX_FILES_OPEN = 5 +DEFAULT_CORE_MEMORY_SOURCE_CHAR_LIMIT: int = 50000 +# Max values for file controls (int32 limit to match database INTEGER type) +MAX_INT32: int = 2147483647 +MAX_PER_FILE_VIEW_WINDOW_CHAR_LIMIT: int = MAX_INT32 +MAX_FILES_OPEN_LIMIT: int = 1000 # Practical limit - no agent needs 1000+ files open + +GET_PROVIDERS_TIMEOUT_SECONDS = 10 + +# Pinecone related fields +PINECONE_EMBEDDING_MODEL: str = "llama-text-embed-v2" +PINECONE_TEXT_FIELD_NAME = "chunk_text" +PINECONE_METRIC = "cosine" +PINECONE_CLOUD = "aws" +PINECONE_REGION = "us-east-1" +PINECONE_MAX_BATCH_SIZE = 96 + +# retry configuration +PINECONE_MAX_RETRY_ATTEMPTS = 3 +PINECONE_RETRY_BASE_DELAY = 1.0 # seconds +PINECONE_RETRY_MAX_DELAY = 60.0 # seconds +PINECONE_RETRY_BACKOFF_FACTOR = 2.0 +PINECONE_THROTTLE_DELAY = 0.75 # seconds base delay between batches + +# builtin web search +WEB_SEARCH_MODEL_ENV_VAR_NAME = "LETTA_BUILTIN_WEBSEARCH_OPENAI_MODEL_NAME" +WEB_SEARCH_MODEL_ENV_VAR_DEFAULT_VALUE = "gpt-4.1-mini-2025-04-14" + +# Excluded model keywords from base tool rules +EXCLUDE_MODEL_KEYWORDS_FROM_BASE_TOOL_RULES = ["claude-4-sonnet", "claude-3-5-sonnet", "gpt-5", "gemini-2.5-pro"] +# But include models with these keywords in base tool rules (overrides exclusion) +INCLUDE_MODEL_KEYWORDS_BASE_TOOL_RULES = ["mini"] + +# Deployment and versioning +MODAL_DEFAULT_TOOL_NAME = "modal_tool_wrapper..modal_function" # NOTE: must stay in sync with modal_tool_wrapper +MODAL_DEFAULT_CONFIG_KEY = "default" +MODAL_MODAL_DEPLOYMENTS_KEY = "modal_deployments" +MODAL_VERSION_HASH_LENGTH = 12 + +# Modal execution settings +MODAL_DEFAULT_TIMEOUT = 60 +MODAL_DEFAULT_MAX_CONCURRENT_INPUTS = 1 +MODAL_DEFAULT_PYTHON_VERSION = "3.12" + +# Security settings +MODAL_SAFE_IMPORT_MODULES = {"typing", "pydantic", "datetime", "uuid"} # decimal, enum +# Default handle for model used to generate tools +DEFAULT_GENERATE_TOOL_MODEL_HANDLE = "openai/gpt-4.1" + +# Reserved keyword arguments that are injected by the system into tool functions, not provided by the LLM +# These parameters are excluded from tool schema generation +TOOL_RESERVED_KWARGS = ["self", "agent_state"] diff --git a/letta/data_sources/__init__.py b/letta/data_sources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/letta/data_sources/connectors.py b/letta/data_sources/connectors.py new file mode 100644 index 0000000..1cfd2b2 --- /dev/null +++ b/letta/data_sources/connectors.py @@ -0,0 +1,213 @@ +from typing import TYPE_CHECKING, Dict, Iterator, List, Tuple + +if TYPE_CHECKING: + from letta.schemas.user import User + +import typer + +from letta.constants import EMBEDDING_BATCH_SIZE +from letta.data_sources.connectors_helper import assert_all_files_exist_locally, extract_metadata_from_files, get_filenames_in_dir +from letta.schemas.file import FileMetadata +from letta.schemas.passage import Passage +from letta.schemas.source import Source +from letta.services.file_manager import FileManager +from letta.services.passage_manager import PassageManager + + +class DataConnector: + """ + Base class for data connectors that can be extended to generate files and passages from a custom data source. + """ + + def find_files(self, source: Source) -> Iterator[FileMetadata]: + """ + Generate file metadata from a data source. + + Returns: + files (Iterator[FileMetadata]): Generate file metadata for each file found. + """ + + def generate_passages(self, file: FileMetadata, chunk_size: int = 1024) -> Iterator[Tuple[str, Dict]]: # -> Iterator[Passage]: + """ + Generate passage text and metadata from a list of files. + + Args: + file (FileMetadata): The document to generate passages from. + chunk_size (int, optional): Chunk size for splitting passages. Defaults to 1024. + + Returns: + passages (Iterator[Tuple[str, Dict]]): Generate a tuple of string text and metadata dictionary for each passage. + """ + + +async def load_data(connector: DataConnector, source: Source, passage_manager: PassageManager, file_manager: FileManager, actor: "User"): + from letta.llm_api.llm_client import LLMClient + + """Load data from a connector (generates file and passages) into a specified source_id, associated with a user_id.""" + embedding_config = source.embedding_config + + # insert passages/file + embedding_to_document_name = {} + passage_count = 0 + file_count = 0 + + # Use the new LLMClient for all embedding requests + client = LLMClient.create( + provider_type=embedding_config.embedding_endpoint_type, + actor=actor, + ) + + for file_metadata in connector.find_files(source): + file_count += 1 + await file_manager.create_file(file_metadata, actor) + + # generate passages for this file + texts = [] + metadatas = [] + + for passage_text, passage_metadata in connector.generate_passages(file_metadata, chunk_size=embedding_config.embedding_chunk_size): + # for some reason, llama index parsers sometimes return empty strings + if len(passage_text) == 0: + typer.secho( + f"Warning: Llama index parser returned empty string, skipping insert of passage with metadata '{passage_metadata}' into VectorDB. You can usually ignore this warning.", + fg=typer.colors.YELLOW, + ) + continue + + texts.append(passage_text) + metadatas.append(passage_metadata) + + if len(texts) >= EMBEDDING_BATCH_SIZE: + # Process the batch + embeddings = await client.request_embeddings(texts, embedding_config) + passages = [] + + for text, embedding, passage_metadata in zip(texts, embeddings, metadatas): + passage = Passage( + text=text, + file_id=file_metadata.id, + source_id=source.id, + metadata=passage_metadata, + organization_id=source.organization_id, + embedding_config=source.embedding_config, + embedding=embedding, + ) + hashable_embedding = tuple(passage.embedding) + file_name = file_metadata.file_name + if hashable_embedding in embedding_to_document_name: + typer.secho( + f"Warning: Duplicate embedding found for passage in {file_name} (already exists in {embedding_to_document_name[hashable_embedding]}), skipping insert into VectorDB.", + fg=typer.colors.YELLOW, + ) + continue + + passages.append(passage) + embedding_to_document_name[hashable_embedding] = file_name + + # insert passages into passage store + await passage_manager.create_many_passages_async(passages, actor) + passage_count += len(passages) + + # Reset for next batch + texts = [] + metadatas = [] + + # Process final remaining texts for this file + if len(texts) > 0: + embeddings = await client.request_embeddings(texts, embedding_config) + passages = [] + + for text, embedding, passage_metadata in zip(texts, embeddings, metadatas): + passage = Passage( + text=text, + file_id=file_metadata.id, + source_id=source.id, + metadata=passage_metadata, + organization_id=source.organization_id, + embedding_config=source.embedding_config, + embedding=embedding, + ) + hashable_embedding = tuple(passage.embedding) + file_name = file_metadata.file_name + if hashable_embedding in embedding_to_document_name: + typer.secho( + f"Warning: Duplicate embedding found for passage in {file_name} (already exists in {embedding_to_document_name[hashable_embedding]}), skipping insert into VectorDB.", + fg=typer.colors.YELLOW, + ) + continue + + passages.append(passage) + embedding_to_document_name[hashable_embedding] = file_name + + await passage_manager.create_many_passages_async(passages, actor) + passage_count += len(passages) + + return passage_count, file_count + + +class DirectoryConnector(DataConnector): + def __init__( + self, + input_files: List[str] | None = None, + input_directory: str | None = None, + recursive: bool = False, + extensions: List[str] | None = None, + ): + """ + Connector for reading text data from a directory of files. + + Args: + input_files (List[str], optional): List of file paths to read. Defaults to None. + input_directory (str, optional): Directory to read files from. Defaults to None. + recursive (bool, optional): Whether to read files recursively from the input directory. Defaults to False. + extensions (List[str], optional): List of file extensions to read. Defaults to None. + """ + self.connector_type = "directory" + self.input_files = input_files + self.input_directory = input_directory + self.recursive = recursive + self.extensions = extensions + + if self.recursive: + assert self.input_directory is not None, "Must provide input directory if recursive is True." + + def find_files(self, source: Source) -> Iterator[FileMetadata]: + if self.input_directory is not None: + files = get_filenames_in_dir( + input_dir=self.input_directory, + recursive=self.recursive, + required_exts=[ext.strip() for ext in str(self.extensions).split(",")], + exclude=["*png", "*jpg", "*jpeg"], + ) + else: + files = self.input_files + + # Check that file paths are valid + assert_all_files_exist_locally(files) + + for metadata in extract_metadata_from_files(files): + yield FileMetadata( + source_id=source.id, + file_name=metadata.get("file_name"), + file_path=metadata.get("file_path"), + file_type=metadata.get("file_type"), + file_size=metadata.get("file_size"), + file_creation_date=metadata.get("file_creation_date"), + file_last_modified_date=metadata.get("file_last_modified_date"), + ) + + def generate_passages(self, file: FileMetadata, chunk_size: int = 1024) -> Iterator[Tuple[str, Dict]]: + from llama_index.core import SimpleDirectoryReader + from llama_index.core.node_parser import TokenTextSplitter + + parser = TokenTextSplitter(chunk_size=chunk_size) + if file.file_type == "application/pdf": + from llama_index.readers.file import PDFReader + + reader = PDFReader() + documents = reader.load_data(file=file.file_path) + else: + documents = SimpleDirectoryReader(input_files=[file.file_path]).load_data() + nodes = parser.get_nodes_from_documents(documents) + for node in nodes: + yield node.text, None diff --git a/letta/data_sources/connectors_helper.py b/letta/data_sources/connectors_helper.py new file mode 100644 index 0000000..95d3dbf --- /dev/null +++ b/letta/data_sources/connectors_helper.py @@ -0,0 +1,97 @@ +import mimetypes +import os +from datetime import datetime +from pathlib import Path +from typing import List, Optional + + +def extract_file_metadata(file_path) -> dict: + """Extracts metadata from a single file.""" + if not os.path.exists(file_path): + raise FileNotFoundError(file_path) + + file_metadata = { + "file_name": os.path.basename(file_path), + "file_path": file_path, + "file_type": mimetypes.guess_type(file_path)[0] or "unknown", + "file_size": os.path.getsize(file_path), + "file_creation_date": datetime.fromtimestamp(os.path.getctime(file_path)).strftime("%Y-%m-%d"), + "file_last_modified_date": datetime.fromtimestamp(os.path.getmtime(file_path)).strftime("%Y-%m-%d"), + } + return file_metadata + + +def extract_metadata_from_files(file_list): + """Extracts metadata for a list of files.""" + metadata = [] + for file_path in file_list: + file_metadata = extract_file_metadata(file_path) + if file_metadata: + metadata.append(file_metadata) + return metadata + + +def get_filenames_in_dir( + input_dir: str, recursive: bool = True, required_exts: Optional[List[str]] = None, exclude: Optional[List[str]] = None +): + """ + Recursively reads files from the directory, applying required_exts and exclude filters. + Ensures that required_exts and exclude do not overlap. + + Args: + input_dir (str): The directory to scan for files. + recursive (bool): Whether to scan directories recursively. + required_exts (list): List of file extensions to include (e.g., ['pdf', 'txt']). + If None or empty, matches any file extension. + exclude (list): List of file patterns to exclude (e.g., ['*png', '*jpg']). + + Returns: + list: A list of matching file paths. + """ + required_exts = required_exts or [] + exclude = exclude or [] + + # Ensure required_exts and exclude do not overlap + ext_set = set(required_exts) + exclude_set = set(exclude) + overlap = ext_set & exclude_set + if overlap: + raise ValueError(f"Extensions in required_exts and exclude overlap: {overlap}") + + def is_excluded(file_name): + """Check if a file matches any pattern in the exclude list.""" + for pattern in exclude: + if Path(file_name).match(pattern): + return True + return False + + files = [] + search_pattern = "**/*" if recursive else "*" + + for file_path in Path(input_dir).glob(search_pattern): + if file_path.is_file() and not is_excluded(file_path.name): + ext = file_path.suffix.lstrip(".") + # If required_exts is empty, match any file + if not required_exts or ext in required_exts: + files.append(str(file_path)) + + return files + + +def assert_all_files_exist_locally(file_paths: List[str]) -> bool: + """ + Checks if all file paths in the provided list exist locally. + Raises a FileNotFoundError with a list of missing files if any do not exist. + + Args: + file_paths (List[str]): List of file paths to check. + + Returns: + bool: True if all files exist, raises FileNotFoundError if any file is missing. + """ + missing_files = [file_path for file_path in file_paths if not Path(file_path).exists()] + + if missing_files: + raise FileNotFoundError(missing_files) + + return True diff --git a/letta/data_sources/redis_client.py b/letta/data_sources/redis_client.py new file mode 100644 index 0000000..25808a4 --- /dev/null +++ b/letta/data_sources/redis_client.py @@ -0,0 +1,681 @@ +import asyncio +from functools import wraps +from typing import Any, Dict, List, Optional, Set, Union + +from letta.constants import ( + CONVERSATION_LOCK_PREFIX, + CONVERSATION_LOCK_TTL_SECONDS, + MEMORY_REPO_LOCK_PREFIX, + MEMORY_REPO_LOCK_TTL_SECONDS, + OTID_RUN_PREFIX, + OTID_RUN_TTL_SECONDS, + REDIS_EXCLUDE, + REDIS_INCLUDE, + REDIS_SET_DEFAULT_VAL, +) +from letta.errors import ConversationBusyError, MemoryRepoBusyError +from letta.log import get_logger +from letta.otel.metric_registry import MetricRegistry +from letta.settings import settings + +try: + from redis import RedisError + from redis.asyncio import ConnectionPool, Redis + from redis.asyncio.lock import Lock +except ImportError: + RedisError = None + Redis = None + ConnectionPool = None + Lock = None + +logger = get_logger(__name__) + +_client_instance = None + + +class AsyncRedisClient: + """Async Redis client with connection pooling and error handling""" + + def __init__( + self, + host: str = "localhost", + port: int = 6379, + db: int = 0, + password: Optional[str] = None, + max_connections: int = 50, + decode_responses: bool = True, + socket_timeout: int = 5, + socket_connect_timeout: int = 5, + retry_on_timeout: bool = True, + health_check_interval: int = 30, + ): + """ + Initialize Redis client with connection pool. + + Args: + host: Redis server hostname + port: Redis server port + db: Database number + password: Redis password if required + max_connections: Maximum number of connections in pool + decode_responses: Decode byte responses to strings + socket_timeout: Socket timeout in seconds + socket_connect_timeout: Socket connection timeout + retry_on_timeout: Retry operations on timeout + health_check_interval: Seconds between health checks + """ + self.pool = ConnectionPool( + host=host, + port=port, + db=db, + password=password, + max_connections=max_connections, + decode_responses=decode_responses, + socket_timeout=socket_timeout, + socket_connect_timeout=socket_connect_timeout, + retry_on_timeout=retry_on_timeout, + health_check_interval=health_check_interval, + ) + self._client = None + self._lock = asyncio.Lock() + + async def get_client(self) -> Redis: + """Get or create Redis client instance.""" + if self._client is None: + async with self._lock: + if self._client is None: + self._client = Redis(connection_pool=self.pool) + return self._client + + async def close(self): + """Close Redis connection and cleanup.""" + if self._client: + await self._client.close() + await self.pool.disconnect() + self._client = None + + async def __aenter__(self): + """Async context manager entry.""" + await self.get_client() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + await self.close() + + # Health check and connection management + async def ping(self) -> bool: + """Check if Redis is accessible.""" + try: + client = await self.get_client() + await client.ping() + return True + except RedisError: + logger.exception("Redis ping failed") + return False + + async def wait_for_ready(self, timeout: int = 30, interval: float = 0.5): + """Wait for Redis to be ready.""" + start_time = asyncio.get_event_loop().time() + while (asyncio.get_event_loop().time() - start_time) < timeout: + if await self.ping(): + return + await asyncio.sleep(interval) + raise ConnectionError(f"Redis not ready after {timeout} seconds") + + # Retry decorator for resilience + def with_retry(max_attempts: int = 3, delay: float = 0.1): + """Decorator to retry Redis operations on failure.""" + + def decorator(func): + @wraps(func) + async def wrapper(self, *args, **kwargs): + last_error = None + for attempt in range(max_attempts): + try: + return await func(self, *args, **kwargs) + except TimeoutError as e: + MetricRegistry().redis_timeout_counter.add(1, attributes={"operation": func.__name__}) + last_error = e + if attempt < max_attempts - 1: + await asyncio.sleep(delay * (2**attempt)) + logger.warning(f"Retry {attempt + 1}/{max_attempts} for {func.__name__}: {e}") + except ConnectionError as e: + last_error = e + if attempt < max_attempts - 1: + await asyncio.sleep(delay * (2**attempt)) + logger.warning(f"Retry {attempt + 1}/{max_attempts} for {func.__name__}: {e}") + raise last_error + + return wrapper + + return decorator + + # Basic operations with error handling + @with_retry() + async def get(self, key: str, default: Any = None) -> Any: + """Get value by key.""" + try: + client = await self.get_client() + return await client.get(key) + except Exception: + return default + + @with_retry() + async def set( + self, + key: str, + value: Union[str, int, float], + ex: Optional[int] = None, + px: Optional[int] = None, + nx: bool = False, + xx: bool = False, + ) -> bool: + """ + Set key-value with options. + + Args: + key: Redis key + value: Value to store + ex: Expire time in seconds + px: Expire time in milliseconds + nx: Only set if key doesn't exist + xx: Only set if key exists + """ + client = await self.get_client() + return await client.set(key, value, ex=ex, px=px, nx=nx, xx=xx) + + @with_retry() + async def delete(self, *keys: str) -> int: + """Delete one or more keys.""" + client = await self.get_client() + return await client.delete(*keys) + + async def acquire_conversation_lock( + self, + conversation_id: str, + token: str, + ) -> Optional["Lock"]: + """ + Acquire a distributed lock for a conversation. + + Args: + conversation_id: The ID for the conversation + token: Unique identifier for the lock holder (for debugging/tracing) + + Returns: + Lock object if acquired, raises ConversationBusyError if in use + """ + if Lock is None: + return None + client = await self.get_client() + lock_key = f"{CONVERSATION_LOCK_PREFIX}{conversation_id}" + lock = Lock( + client, + lock_key, + timeout=CONVERSATION_LOCK_TTL_SECONDS, + blocking=False, + thread_local=False, # We manage token explicitly + raise_on_release_error=False, # We handle release errors ourselves + ) + + if await lock.acquire(token=token): + return lock + + lock_holder_token = await client.get(lock_key) + raise ConversationBusyError( + conversation_id=conversation_id, + lock_holder_token=lock_holder_token, + ) + + async def release_conversation_lock(self, conversation_id: str) -> bool: + """ + Release a conversation lock by conversation_id. + + Args: + conversation_id: The conversation ID to release the lock for + + Returns: + True if lock was released, False if release failed + """ + try: + client = await self.get_client() + lock_key = f"{CONVERSATION_LOCK_PREFIX}{conversation_id}" + await client.delete(lock_key) + return True + except Exception as e: + logger.warning(f"Failed to release conversation lock for conversation {conversation_id}: {e}") + return False + + async def set_otid_run_mapping(self, otid: str, run_id: str) -> bool: + """ + Store a mapping from otid to run_id. + + This allows recovering the run_id from a duplicate request's otid + when a 409 Conflict error is raised. + + Args: + otid: The offline threading ID (used as lock token) + run_id: The run ID associated with this request + + Returns: + True if mapping was stored successfully + """ + try: + client = await self.get_client() + key = f"{OTID_RUN_PREFIX}{otid}" + await client.set(key, run_id, ex=OTID_RUN_TTL_SECONDS) + return True + except Exception as e: + logger.warning(f"Failed to store otid->run_id mapping for otid {otid}: {e}") + return False + + async def get_run_id_by_otid(self, otid: str) -> Optional[str]: + """ + Look up the run_id associated with an otid. + + Args: + otid: The offline threading ID to look up + + Returns: + The run_id if found, None otherwise + """ + try: + client = await self.get_client() + key = f"{OTID_RUN_PREFIX}{otid}" + return await client.get(key) + except Exception as e: + logger.warning(f"Failed to lookup run_id for otid {otid}: {e}") + return None + + async def has_stream_chunks(self, run_id: str) -> bool: + """ + Check if there are any chunks available for a run in Redis. + + Args: + run_id: The run ID to check + + Returns: + True if chunks exist, False otherwise + """ + try: + client = await self.get_client() + stream_key = f"sse:run:{run_id}" + # Check if stream exists and has entries + length = await client.xlen(stream_key) + return length > 0 + except Exception as e: + logger.warning(f"Failed to check stream chunks for run {run_id}: {e}") + return False + + async def acquire_memory_repo_lock( + self, + agent_id: str, + token: str, + ) -> Optional["Lock"]: + """ + Acquire a distributed lock for a memory repository. + + Prevents concurrent modifications to an agent's git-based memory. + + Args: + agent_id: The agent ID whose memory is being modified + token: Unique identifier for the lock holder (for debugging/tracing) + + Returns: + Lock object if acquired, raises MemoryRepoBusyError if in use + """ + if Lock is None: + return None + client = await self.get_client() + lock_key = f"{MEMORY_REPO_LOCK_PREFIX}{agent_id}" + lock = Lock( + client, + lock_key, + timeout=MEMORY_REPO_LOCK_TTL_SECONDS, + blocking=False, + thread_local=False, + raise_on_release_error=False, + ) + + if await lock.acquire(token=token): + return lock + + lock_holder_token = await client.get(lock_key) + raise MemoryRepoBusyError( + agent_id=agent_id, + lock_holder_token=lock_holder_token, + ) + + async def release_memory_repo_lock(self, agent_id: str) -> bool: + """ + Release a memory repo lock by agent_id. + + Args: + agent_id: The agent ID to release the lock for + + Returns: + True if lock was released, False if release failed + """ + try: + client = await self.get_client() + lock_key = f"{MEMORY_REPO_LOCK_PREFIX}{agent_id}" + await client.delete(lock_key) + return True + except Exception as e: + logger.warning(f"Failed to release memory repo lock for agent {agent_id}: {e}") + return False + + @with_retry() + async def exists(self, *keys: str) -> int: + """Check if keys exist.""" + client = await self.get_client() + return await client.exists(*keys) + + # Set operations + async def sadd(self, key: str, *members: Union[str, int, float]) -> int: + """Add members to set.""" + client = await self.get_client() + return await client.sadd(key, *members) + + async def smembers(self, key: str) -> Set[str]: + """Get all set members.""" + client = await self.get_client() + return await client.smembers(key) + + @with_retry() + async def smismember(self, key: str, values: list[Any] | Any) -> list[int] | int: + """clever!: set member is member""" + try: + client = await self.get_client() + result = await client.smismember(key, values) + return result if isinstance(values, list) else result[0] + except Exception: + return [0] * len(values) if isinstance(values, list) else 0 + + async def srem(self, key: str, *members: Union[str, int, float]) -> int: + """Remove members from set.""" + client = await self.get_client() + return await client.srem(key, *members) + + async def scard(self, key: str) -> int: + client = await self.get_client() + return await client.scard(key) + + # Atomic operations + async def incr(self, key: str) -> int: + """Increment key value.""" + client = await self.get_client() + return await client.incr(key) + + async def decr(self, key: str) -> int: + """Decrement key value.""" + client = await self.get_client() + return await client.decr(key) + + # Stream operations + @with_retry() + async def xadd(self, stream: str, fields: Dict[str, Any], id: str = "*", maxlen: Optional[int] = None, approximate: bool = True) -> str: + """Add entry to a stream. + + Args: + stream: Stream name + fields: Dict of field-value pairs to add + id: Entry ID ('*' for auto-generation) + maxlen: Maximum length of the stream + approximate: Whether maxlen is approximate + + Returns: + The ID of the added entry + """ + client = await self.get_client() + return await client.xadd(stream, fields, id=id, maxlen=maxlen, approximate=approximate) + + @with_retry() + async def xread(self, streams: Dict[str, str], count: Optional[int] = None, block: Optional[int] = None) -> List[Dict]: + """Read from streams. + + Args: + streams: Dict mapping stream names to IDs + count: Maximum number of entries to return + block: Milliseconds to block waiting for data (None = no blocking) + + Returns: + List of entries from the streams + """ + client = await self.get_client() + return await client.xread(streams, count=count, block=block) + + @with_retry() + async def xrange(self, stream: str, start: str = "-", end: str = "+", count: Optional[int] = None) -> List[Dict]: + """Read range of entries from a stream. + + Args: + stream: Stream name + start: Start ID (inclusive) + end: End ID (inclusive) + count: Maximum number of entries to return + + Returns: + List of entries in the specified range + """ + client = await self.get_client() + return await client.xrange(stream, start, end, count=count) + + @with_retry() + async def xrevrange(self, stream: str, start: str = "+", end: str = "-", count: Optional[int] = None) -> List[Dict]: + """Read range of entries from a stream in reverse order. + + Args: + stream: Stream name + start: Start ID (inclusive) + end: End ID (inclusive) + count: Maximum number of entries to return + + Returns: + List of entries in the specified range in reverse order + """ + client = await self.get_client() + return await client.xrevrange(stream, start, end, count=count) + + @with_retry() + async def xlen(self, stream: str) -> int: + """Get the length of a stream. + + Args: + stream: Stream name + + Returns: + Number of entries in the stream + """ + client = await self.get_client() + return await client.xlen(stream) + + @with_retry() + async def xdel(self, stream: str, *ids: str) -> int: + """Delete entries from a stream. + + Args: + stream: Stream name + ids: IDs of entries to delete + + Returns: + Number of entries deleted + """ + client = await self.get_client() + return await client.xdel(stream, *ids) + + @with_retry() + async def xinfo_stream(self, stream: str) -> Dict: + """Get information about a stream. + + Args: + stream: Stream name + + Returns: + Dict with stream information + """ + client = await self.get_client() + return await client.xinfo_stream(stream) + + @with_retry() + async def xtrim(self, stream: str, maxlen: int, approximate: bool = True) -> int: + """Trim a stream to a maximum length. + + Args: + stream: Stream name + maxlen: Maximum length + approximate: Whether maxlen is approximate + + Returns: + Number of entries removed + """ + client = await self.get_client() + return await client.xtrim(stream, maxlen=maxlen, approximate=approximate) + + async def check_inclusion_and_exclusion(self, member: str, group: str) -> bool: + exclude_key = self._get_group_exclusion_key(group) + include_key = self._get_group_inclusion_key(group) + # 1. if the member IS excluded from the group + if self.exists(exclude_key) and await self.scard(exclude_key) > 1: + return bool(await self.smismember(exclude_key, member)) + # 2. if the group HAS an include set, is the member in that set? + if self.exists(include_key) and await self.scard(include_key) > 1: + return bool(await self.smismember(include_key, member)) + # 3. if the group does NOT HAVE an include set and member NOT excluded + return True + + async def create_inclusion_exclusion_keys(self, group: str) -> None: + redis_client = await self.get_client() + await redis_client.sadd(self._get_group_inclusion_key(group), REDIS_SET_DEFAULT_VAL) + await redis_client.sadd(self._get_group_exclusion_key(group), REDIS_SET_DEFAULT_VAL) + + @staticmethod + def _get_group_inclusion_key(group: str) -> str: + return f"{group}:{REDIS_INCLUDE}" + + @staticmethod + def _get_group_exclusion_key(group: str) -> str: + return f"{group}:{REDIS_EXCLUDE}" + + +class NoopAsyncRedisClient(AsyncRedisClient): + # noinspection PyMissingConstructor + def __init__(self): + pass + + async def set( + self, + key: str, + value: Union[str, int, float], + ex: Optional[int] = None, + px: Optional[int] = None, + nx: bool = False, + xx: bool = False, + ) -> bool: + return False + + async def get(self, key: str, default: Any = None) -> Any: + return default + + async def exists(self, *keys: str) -> int: + return 0 + + async def sadd(self, key: str, *members: Union[str, int, float]) -> int: + return 0 + + async def smismember(self, key: str, values: list[Any] | Any) -> list[int] | int: + return [0] * len(values) if isinstance(values, list) else 0 + + async def delete(self, *keys: str) -> int: + return 0 + + async def acquire_conversation_lock( + self, + conversation_id: str, + token: str, + ) -> Optional["Lock"]: + return None + + async def release_conversation_lock(self, conversation_id: str) -> bool: + return False + + async def set_otid_run_mapping(self, otid: str, run_id: str) -> bool: + return False + + async def get_run_id_by_otid(self, otid: str) -> Optional[str]: + return None + + async def has_stream_chunks(self, run_id: str) -> bool: + return False + + async def acquire_memory_repo_lock( + self, + agent_id: str, + token: str, + ) -> Optional["Lock"]: + return None + + async def release_memory_repo_lock(self, agent_id: str) -> bool: + return False + + async def check_inclusion_and_exclusion(self, member: str, group: str) -> bool: + return False + + async def create_inclusion_exclusion_keys(self, group: str) -> None: + return None + + async def scard(self, key: str) -> int: + return 0 + + async def smembers(self, key: str) -> Set[str]: + return set() + + async def srem(self, key: str, *members: Union[str, int, float]) -> int: + return 0 + + # Stream operations + async def xadd(self, stream: str, fields: Dict[str, Any], id: str = "*", maxlen: Optional[int] = None, approximate: bool = True) -> str: + return "" + + async def xread(self, streams: Dict[str, str], count: Optional[int] = None, block: Optional[int] = None) -> List[Dict]: + return [] + + async def xrange(self, stream: str, start: str = "-", end: str = "+", count: Optional[int] = None) -> List[Dict]: + return [] + + async def xrevrange(self, stream: str, start: str = "+", end: str = "-", count: Optional[int] = None) -> List[Dict]: + return [] + + async def xlen(self, stream: str) -> int: + return 0 + + async def xdel(self, stream: str, *ids: str) -> int: + return 0 + + async def xinfo_stream(self, stream: str) -> Dict: + return {} + + async def xtrim(self, stream: str, maxlen: int, approximate: bool = True) -> int: + return 0 + + +async def get_redis_client() -> AsyncRedisClient: + global _client_instance + if _client_instance is None: + try: + # If Redis settings are not configured, use noop client + if settings.redis_host is None or settings.redis_port is None: + logger.info("Redis not configured, using noop client") + _client_instance = NoopAsyncRedisClient() + else: + _client_instance = AsyncRedisClient( + host=settings.redis_host, + port=settings.redis_port, + ) + await _client_instance.wait_for_ready(timeout=5) + logger.info("Redis client initialized") + except Exception as e: + logger.warning(f"Failed to initialize Redis: {e}") + _client_instance = NoopAsyncRedisClient() + return _client_instance diff --git a/letta/database_utils.py b/letta/database_utils.py new file mode 100644 index 0000000..f5c499c --- /dev/null +++ b/letta/database_utils.py @@ -0,0 +1,161 @@ +""" +Database URI utilities for consistent database connection handling across the application. + +This module provides utilities for parsing and converting database URIs to ensure +consistent behavior between the main application, alembic migrations, and other +database-related components. +""" + +from typing import Optional +from urllib.parse import urlparse, urlunparse + + +def parse_database_uri(uri: str) -> dict[str, Optional[str]]: + """ + Parse a database URI into its components. + + Args: + uri: Database URI (e.g., postgresql://user:pass@host:port/db) + + Returns: + Dictionary with parsed components: scheme, driver, user, password, host, port, database + """ + parsed = urlparse(uri) + + # Extract driver from scheme (e.g., postgresql+asyncpg -> asyncpg) + scheme_parts = parsed.scheme.split("+") + base_scheme = scheme_parts[0] if scheme_parts else "" + driver = scheme_parts[1] if len(scheme_parts) > 1 else None + + return { + "scheme": base_scheme, + "driver": driver, + "user": parsed.username, + "password": parsed.password, + "host": parsed.hostname, + "port": str(parsed.port) if parsed.port else None, + "database": parsed.path.lstrip("/") if parsed.path else None, + "query": parsed.query, + "fragment": parsed.fragment, + } + + +def build_database_uri( + scheme: str = "postgresql", + driver: Optional[str] = None, + user: Optional[str] = None, + password: Optional[str] = None, + host: Optional[str] = None, + port: Optional[str] = None, + database: Optional[str] = None, + query: Optional[str] = None, + fragment: Optional[str] = None, +) -> str: + """ + Build a database URI from components. + + Args: + scheme: Base scheme (e.g., "postgresql") + driver: Driver name (e.g., "asyncpg", "pg8000") + user: Username + password: Password + host: Hostname + port: Port number + database: Database name + query: Query string + fragment: Fragment + + Returns: + Complete database URI + """ + # Combine scheme and driver + full_scheme = f"{scheme}+{driver}" if driver else scheme + + # Build netloc (user:password@host:port) + netloc_parts = [] + if user: + if password: + netloc_parts.append(f"{user}:{password}") + else: + netloc_parts.append(user) + + if host: + if port: + netloc_parts.append(f"{host}:{port}") + else: + netloc_parts.append(host) + + netloc = "@".join(netloc_parts) if netloc_parts else "" + + # Build path + path = f"/{database}" if database else "" + + # Build the URI + return urlunparse((full_scheme, netloc, path, "", query or "", fragment or "")) + + +def convert_to_async_uri(uri: str) -> str: + """ + Convert a database URI to use the asyncpg driver for async operations. + + Args: + uri: Original database URI + + Returns: + URI with asyncpg driver and ssl parameter adjustments + """ + components = parse_database_uri(uri) + + # Convert to asyncpg driver + components["driver"] = "asyncpg" + + # Build the new URI + new_uri = build_database_uri(**components) + + # Replace sslmode= with ssl= for asyncpg compatibility + new_uri = new_uri.replace("sslmode=", "ssl=") + + return new_uri + + +def convert_to_sync_uri(uri: str) -> str: + """ + Convert a database URI to use the pg8000 driver for sync operations (alembic). + + Args: + uri: Original database URI + + Returns: + URI with pg8000 driver and sslmode parameter adjustments + """ + components = parse_database_uri(uri) + + # Convert to pg8000 driver + components["driver"] = "pg8000" + + # Build the new URI + new_uri = build_database_uri(**components) + + # Replace ssl= with sslmode= for pg8000 compatibility + new_uri = new_uri.replace("ssl=", "sslmode=") + + return new_uri + + +def get_database_uri_for_context(uri: str, context: str = "async") -> str: + """ + Get the appropriate database URI for a specific context. + + Args: + uri: Original database URI + context: Context type ("async" for asyncpg, "sync" for pg8000, "alembic" for pg8000) + + Returns: + URI formatted for the specified context + """ + if context in ["async"]: + return convert_to_async_uri(uri) + elif context in ["sync", "alembic"]: + return convert_to_sync_uri(uri) + else: + raise ValueError(f"Unknown context: {context}. Must be 'async', 'sync', or 'alembic'") diff --git a/letta/errors.py b/letta/errors.py new file mode 100644 index 0000000..6f16592 --- /dev/null +++ b/letta/errors.py @@ -0,0 +1,492 @@ +import json +from enum import Enum +from typing import TYPE_CHECKING, Dict, List, Optional, Union + +# Avoid circular imports +if TYPE_CHECKING: + from letta.schemas.letta_message import LettaMessage + from letta.schemas.message import Message + + +class ErrorCode(Enum): + """Enum for error codes used by client.""" + + NOT_FOUND = "NOT_FOUND" + UNAUTHENTICATED = "UNAUTHENTICATED" + PERMISSION_DENIED = "PERMISSION_DENIED" + INVALID_ARGUMENT = "INVALID_ARGUMENT" + INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR" + CONTEXT_WINDOW_EXCEEDED = "CONTEXT_WINDOW_EXCEEDED" + RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED" + TIMEOUT = "TIMEOUT" + CONFLICT = "CONFLICT" + EXPIRED = "EXPIRED" + PAYMENT_REQUIRED = "PAYMENT_REQUIRED" + + +class LettaError(Exception): + """Base class for all Letta related errors.""" + + def __init__(self, message: str, code: Optional[ErrorCode] = None, details: Optional[Union[Dict, str, object]] = None): + if details is None: + details = {} + self.message = message + self.code = code + self.details = details + super().__init__(message) + + def __str__(self) -> str: + base = f"{self.code.value}: {self.message}" if self.code else self.message + if isinstance(self.details, dict) and self.details.get("is_byok"): + return f"{base} [BYOK]" + return base + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(message='{self.message}', code='{self.code}', details={self.details})" + + +class PendingApprovalError(LettaError): + """Error raised when attempting an operation while agent is waiting for tool approval.""" + + def __init__(self, pending_request_id: Optional[str] = None): + self.pending_request_id = pending_request_id + message = "Cannot send a new message: The agent is waiting for approval on a tool call. Please approve or deny the pending request before continuing." + code = ErrorCode.CONFLICT + details = {"error_code": "PENDING_APPROVAL", "pending_request_id": pending_request_id} + super().__init__(message=message, code=code, details=details) + + +class NoActiveRunsToCancelError(LettaError): + """Error raised when attempting to cancel but there are no active runs to cancel.""" + + def __init__(self, agent_id: Optional[str] = None, conversation_id: Optional[str] = None): + message = "No active runs to cancel" + if agent_id: + message = f"No active runs to cancel for agent {agent_id}" + if conversation_id: + message = f"No active runs to cancel for conversation {conversation_id}" + details = {"error_code": "NO_ACTIVE_RUNS_TO_CANCEL", "agent_id": agent_id, "conversation_id": conversation_id} + super().__init__(message=message, code=ErrorCode.CONFLICT, details=details) + + +class ConcurrentUpdateError(LettaError): + """Error raised when a resource was updated by another transaction (optimistic locking conflict).""" + + def __init__(self, resource_type: str, resource_id: str): + message = f"{resource_type} with id '{resource_id}' was updated by another transaction. Please retry your request." + details = {"error_code": "CONCURRENT_UPDATE", "resource_type": resource_type, "resource_id": resource_id} + super().__init__(message=message, code=ErrorCode.CONFLICT, details=details) + + +class ConversationBusyError(LettaError): + """Error raised when attempting to send a message while another request is already processing for the same conversation.""" + + def __init__( + self, + conversation_id: str, + lock_holder_token: Optional[str] = None, + run_id: Optional[str] = None, + ): + self.conversation_id = conversation_id + self.lock_holder_token = lock_holder_token + self.run_id = run_id + + # Build message with available info + if run_id: + message = f"Cannot send a new message: Another request (run_id={run_id}) is currently being processed for this conversation. Please wait for it to complete." + else: + message = "Cannot send a new message: Another request is currently being processed for this conversation. Please wait for the current request to complete." + + code = ErrorCode.CONFLICT + details = { + "error_code": "CONVERSATION_BUSY", + "conversation_id": conversation_id, + } + if run_id: + details["run_id"] = run_id + super().__init__(message=message, code=code, details=details) + + +class MemoryRepoBusyError(LettaError): + """Error raised when attempting to modify memory while another operation is in progress.""" + + def __init__(self, agent_id: str, lock_holder_token: Optional[str] = None): + self.agent_id = agent_id + self.lock_holder_token = lock_holder_token + message = "Cannot modify memory: Another operation is currently in progress for this agent's memory. Please wait for the current operation to complete." + code = ErrorCode.CONFLICT + details = { + "error_code": "MEMORY_REPO_BUSY", + "agent_id": agent_id, + "lock_holder_token": lock_holder_token, + } + super().__init__(message=message, code=code, details=details) + + +class LettaToolCreateError(LettaError): + """Error raised when a tool cannot be created.""" + + default_error_message = "Error creating tool." + + def __init__(self, message=None): + super().__init__(message=message or self.default_error_message) + + +class LettaToolNameConflictError(LettaError): + """Error raised when a tool name already exists.""" + + def __init__(self, tool_name: str): + super().__init__( + message=f"Tool with name '{tool_name}' already exists in your organization", + code=ErrorCode.INVALID_ARGUMENT, + details={"tool_name": tool_name}, + ) + + +class LettaToolNameSchemaMismatchError(LettaToolCreateError): + """Error raised when a tool name our source codedoes not match the name in the JSON schema.""" + + def __init__(self, tool_name: str, json_schema_name: str, source_code: str): + super().__init__( + message=f"Tool name '{tool_name}' does not match the name in the JSON schema '{json_schema_name}' or in the source code `{source_code}`", + ) + + +class LettaConfigurationError(LettaError): + """Error raised when there are configuration-related issues.""" + + def __init__(self, message: str, missing_fields: Optional[List[str]] = None): + self.missing_fields = missing_fields or [] + super().__init__(message=message, details={"missing_fields": self.missing_fields}) + + +class EmbeddingConfigRequiredError(LettaError): + """Error raised when an operation requires embedding_config but the agent doesn't have one configured.""" + + def __init__(self, agent_id: Optional[str] = None, operation: Optional[str] = None): + self.agent_id = agent_id + self.operation = operation + message = "This operation requires an embedding configuration, but the agent does not have one configured." + if operation: + message = f"Operation '{operation}' requires an embedding configuration, but the agent does not have one configured." + details = {"agent_id": agent_id, "operation": operation} + super().__init__(message=message, code=ErrorCode.INVALID_ARGUMENT, details=details) + + +class LettaAgentNotFoundError(LettaError): + """Error raised when an agent is not found.""" + + +class LettaUserNotFoundError(LettaError): + """Error raised when a user is not found.""" + + +class LettaUnsupportedFileUploadError(LettaError): + """Error raised when an unsupported file upload is attempted.""" + + +class LettaInvalidArgumentError(LettaError): + """Error raised when an invalid argument is provided.""" + + def __init__(self, message: str, argument_name: Optional[str] = None): + details = {"argument_name": argument_name} if argument_name else {} + super().__init__(message=message, code=ErrorCode.INVALID_ARGUMENT, details=details) + + +class LettaImageFetchError(LettaError): + """Error raised when fetching an image from a URL fails.""" + + def __init__(self, url: str, reason: str): + details = {"url": url, "reason": reason} + super().__init__( + message=f"Failed to fetch image from {url}: {reason}", + code=ErrorCode.INVALID_ARGUMENT, + details=details, + ) + + +class LettaMCPError(LettaError): + """Base error for MCP-related issues.""" + + +class LettaInvalidMCPSchemaError(LettaMCPError): + """Error raised when an invalid MCP schema is provided.""" + + def __init__(self, server_name: str, mcp_tool_name: str, reasons: List[str]): + details = {"server_name": server_name, "mcp_tool_name": mcp_tool_name, "reasons": reasons} + super().__init__( + message=f"MCP tool {mcp_tool_name} has an invalid schema and cannot be attached - reasons: {reasons}", + code=ErrorCode.INVALID_ARGUMENT, + details=details, + ) + + +class LettaMCPConnectionError(LettaMCPError): + """Error raised when unable to connect to MCP server.""" + + def __init__(self, message: str, server_name: Optional[str] = None): + details = {"server_name": server_name} if server_name else {} + super().__init__(message=message, code=ErrorCode.INTERNAL_SERVER_ERROR, details=details) + + +class LettaMCPTimeoutError(LettaMCPError): + """Error raised when MCP server operation times out.""" + + def __init__(self, message: str, server_name: Optional[str] = None): + details = {"server_name": server_name} if server_name else {} + super().__init__(message=message, code=ErrorCode.TIMEOUT, details=details) + + +class LettaServiceUnavailableError(LettaError): + """Error raised when a required service is unavailable.""" + + def __init__(self, message: str, service_name: Optional[str] = None): + details = {"service_name": service_name} if service_name else {} + super().__init__(message=message, code=ErrorCode.INTERNAL_SERVER_ERROR, details=details) + + +class LettaUnexpectedStreamCancellationError(LettaError): + """Error raised when a streaming request is terminated unexpectedly.""" + + +class LettaExpiredError(LettaError): + """Error raised when a resource has expired.""" + + def __init__(self, message: str): + super().__init__(message=message, code=ErrorCode.EXPIRED) + + +class LLMError(LettaError): + pass + + +class LLMConnectionError(LLMError): + """Error when unable to connect to LLM service""" + + +class LLMRateLimitError(LLMError): + """Error when rate limited by LLM service""" + + +class LLMBadRequestError(LLMError): + """Error when LLM service cannot process request""" + + +class LLMInsufficientCreditsError(LLMError): + """Error when LLM provider reports insufficient credits or quota""" + + +class LLMAuthenticationError(LLMError): + """Error when authentication fails with LLM service""" + + +class LLMPermissionDeniedError(LLMError): + """Error when permission is denied by LLM service""" + + +class LLMNotFoundError(LLMError): + """Error when requested resource is not found""" + + +class LLMUnprocessableEntityError(LLMError): + """Error when request is well-formed but semantically invalid""" + + +class LLMServerError(LLMError): + """Error indicating an internal server error occurred within the LLM service itself + while processing the request.""" + + +class LLMEmptyResponseError(LLMServerError): + """Error when LLM returns an empty response (no content and no tool calls). + + This is a subclass of LLMServerError to maintain retry behavior, but allows + specific handling for empty response cases which may benefit from request + modification before retry. + """ + + +class LLMTimeoutError(LLMError): + """Error when LLM request times out""" + + +class LLMProviderOverloaded(LLMError): + """Error when LLM provider is overloaded""" + + +class BedrockPermissionError(LettaError): + """Exception raised for errors in the Bedrock permission process.""" + + def __init__(self, message="User does not have access to the Bedrock model with the specified ID."): + super().__init__(message=message) + + +class BedrockError(LettaError): + """Exception raised for errors in the Bedrock process.""" + + def __init__(self, message="Error with Bedrock model."): + super().__init__(message=message) + + +class LLMJSONParsingError(LettaError): + """Exception raised for errors in the JSON parsing process.""" + + def __init__(self, message="Error parsing JSON generated by LLM"): + super().__init__(message=message) + + +class LocalLLMError(LettaError): + """Generic catch-all error for local LLM problems""" + + def __init__(self, message="Encountered an error while running local LLM"): + super().__init__(message=message) + + +class LocalLLMConnectionError(LettaError): + """Error for when local LLM cannot be reached with provided IP/port""" + + def __init__(self, message="Could not connect to local LLM"): + super().__init__(message=message) + + +class ContextWindowExceededError(LettaError): + """Error raised when the context window is exceeded but further summarization fails.""" + + def __init__(self, message: str, details: dict = {}): + error_message = f"{message} ({details})" + super().__init__( + message=error_message, + code=ErrorCode.CONTEXT_WINDOW_EXCEEDED, + details=details, + ) + + +class SystemPromptTokenExceededError(ContextWindowExceededError): + """Error raised when the system prompt token estimate exceeds the context window.""" + + def __init__(self, system_prompt_token_estimate: int, context_window: int): + message = f"The system prompt tokens {system_prompt_token_estimate} exceeds the context window {context_window}. Please reduce the size of your system prompt, memory blocks, or increase the context window." + super().__init__( + message=message, details={"system_prompt_token_estimate": system_prompt_token_estimate, "context_window": context_window} + ) + + +class RateLimitExceededError(LettaError): + """Error raised when the llm rate limiter throttles api requests.""" + + def __init__(self, message: str, max_retries: int): + error_message = f"{message} ({max_retries})" + super().__init__( + message=error_message, + code=ErrorCode.RATE_LIMIT_EXCEEDED, + details={"max_retries": max_retries}, + ) + + +class LettaMessageError(LettaError): + """Base error class for handling message-related errors.""" + + messages: List[Union["Message", "LettaMessage"]] + default_error_message: str = "An error occurred with the message." + + def __init__(self, *, messages: List[Union["Message", "LettaMessage"]], explanation: Optional[str] = None) -> None: + error_msg = self.construct_error_message(messages, self.default_error_message, explanation) + super().__init__(error_msg) + self.messages = messages + + @staticmethod + def construct_error_message(messages: List[Union["Message", "LettaMessage"]], error_msg: str, explanation: Optional[str] = None) -> str: + """Helper method to construct a clean and formatted error message.""" + if explanation: + error_msg += f" (Explanation: {explanation})" + + # Pretty print out message JSON + message_json = json.dumps([message.model_dump() for message in messages], indent=4) + return f"{error_msg}\n\n{message_json}" + + +class MissingToolCallError(LettaMessageError): + """Error raised when a message is missing a tool call.""" + + default_error_message = "The message is missing a tool call." + + +class InvalidToolCallError(LettaMessageError): + """Error raised when a message uses an invalid tool call.""" + + default_error_message = "The message uses an invalid tool call or has improper usage of a tool call." + + +class MissingInnerMonologueError(LettaMessageError): + """Error raised when a message is missing an inner monologue.""" + + default_error_message = "The message is missing an inner monologue." + + +class InvalidInnerMonologueError(LettaMessageError): + """Error raised when a message has a malformed inner monologue.""" + + default_error_message = "The message has a malformed inner monologue." + + +class HandleNotFoundError(LettaError): + """Error raised when a handle is not found.""" + + def __init__(self, handle: str, available_handles: List[str]): + super().__init__( + message=f"Handle {handle} not found, must be one of {available_handles}", + code=ErrorCode.NOT_FOUND, + ) + + +class AgentFileExportError(Exception): + """Exception raised during agent file export operations""" + + +class AgentNotFoundForExportError(AgentFileExportError): + """Exception raised when requested agents are not found during export""" + + def __init__(self, missing_ids: List[str]): + self.missing_ids = missing_ids + super().__init__(f"The following agent IDs were not found: {missing_ids}") + + +class AgentExportIdMappingError(AgentFileExportError): + """Exception raised when ID mapping fails during export conversion""" + + def __init__(self, db_id: str, entity_type: str): + self.db_id = db_id + self.entity_type = entity_type + super().__init__( + f"Unexpected new {entity_type} ID '{db_id}' encountered during conversion. " + f"All IDs should have been mapped during agent processing." + ) + + +class AgentExportProcessingError(AgentFileExportError): + """Exception raised when general export processing fails""" + + def __init__(self, message: str, original_error: Optional[Exception] = None): + self.original_error = original_error + super().__init__(f"Export failed: {message}") + + +class AgentFileImportError(Exception): + """Exception raised during agent file import operations""" + + +class InsufficientCreditsError(LettaError): + """Raised when an organization has no remaining credits.""" + + def __init__(self): + super().__init__( + message="Insufficient credits to process this request.", + details={"error_code": "INSUFFICIENT_CREDITS"}, + ) + + +class RunCancelError(LettaError): + """Error raised when a run cannot be cancelled.""" + + def __init__(self, message: str): + super().__init__(message=message) diff --git a/letta/exceptions/logging.py b/letta/exceptions/logging.py new file mode 100644 index 0000000..3b40f0b --- /dev/null +++ b/letta/exceptions/logging.py @@ -0,0 +1,137 @@ +""" +Helper utilities for structured exception logging. +Use these when you need to add context to exceptions before raising them. +""" + +from typing import Any, Dict, Optional + +from letta.log import get_logger + +logger = get_logger(__name__) + + +def log_and_raise( + exception: Exception, + message: str, + context: Optional[Dict[str, Any]] = None, + level: str = "error", +) -> None: + """ + Log an exception with structured context and then raise it. + + This is useful when you want to ensure an exception is logged with + full context before raising it. + + Args: + exception: The exception to log and raise + message: Human-readable message to log + context: Additional context to include in logs (dict) + level: Log level (default: "error") + + Example: + try: + result = some_operation() + except ValueError as e: + log_and_raise( + e, + "Failed to process operation", + context={ + "user_id": user.id, + "operation": "some_operation", + "input": input_data, + } + ) + """ + extra = { + "exception_type": exception.__class__.__name__, + "exception_message": str(exception), + "exception_module": exception.__class__.__module__, + } + + if context: + extra.update(context) + + log_method = getattr(logger, level.lower()) + log_method( + f"{message}: {exception.__class__.__name__}: {str(exception)}", + extra=extra, + exc_info=exception, + ) + + raise exception + + +def log_exception( + exception: Exception, + message: str, + context: Optional[Dict[str, Any]] = None, + level: str = "error", +) -> None: + """ + Log an exception with structured context without raising it. + + Use this when you want to log an exception but handle it gracefully. + + Args: + exception: The exception to log + message: Human-readable message to log + context: Additional context to include in logs (dict) + level: Log level (default: "error") + + Example: + try: + result = some_operation() + except ValueError as e: + log_exception( + e, + "Operation failed, using fallback", + context={"user_id": user.id} + ) + result = fallback_operation() + """ + extra = { + "exception_type": exception.__class__.__name__, + "exception_message": str(exception), + "exception_module": exception.__class__.__module__, + } + + if context: + extra.update(context) + + log_method = getattr(logger, level.lower()) + log_method( + f"{message}: {exception.__class__.__name__}: {str(exception)}", + extra=extra, + exc_info=exception, + ) + + +def add_exception_context(exception: Exception, **context) -> Exception: + """ + Add context to an exception that will be picked up by the global exception handler. + + This attaches a __letta_context__ attribute to the exception with structured data. + The global exception handler will automatically include this context in logs. + + Args: + exception: The exception to add context to + **context: Key-value pairs to add as context + + Returns: + The same exception with context attached + + Example: + try: + result = operation() + except ValueError as e: + raise add_exception_context( + e, + user_id=user.id, + operation="do_thing", + input_data=data, + ) + """ + if not hasattr(exception, "__letta_context__"): + exception.__letta_context__ = {} + exception.__letta_context__.update(context) + return exception diff --git a/letta/functions/__init__.py b/letta/functions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/letta/functions/ast_parsers.py b/letta/functions/ast_parsers.py new file mode 100644 index 0000000..14eed2f --- /dev/null +++ b/letta/functions/ast_parsers.py @@ -0,0 +1,213 @@ +import ast +import builtins +import json +import typing +from typing import Dict, Optional, Tuple + +from letta.errors import LettaToolCreateError +from letta.types import JsonDict + +_ALLOWED_TYPING_NAMES = {name: obj for name, obj in vars(typing).items() if not name.startswith("_")} +_ALLOWED_BUILTIN_TYPES = {name: obj for name, obj in vars(builtins).items() if isinstance(obj, type)} +_ALLOWED_TYPE_NAMES = {**_ALLOWED_TYPING_NAMES, **_ALLOWED_BUILTIN_TYPES, "typing": typing} + + +def _resolve_annotation_node(node: ast.AST): + if isinstance(node, ast.Name): + if node.id == "None": + return type(None) + if node.id in _ALLOWED_TYPE_NAMES: + return _ALLOWED_TYPE_NAMES[node.id] + raise ValueError(f"Unsupported annotation name: {node.id}") + + if isinstance(node, ast.Attribute): + if isinstance(node.value, ast.Name) and node.value.id == "typing" and node.attr in _ALLOWED_TYPING_NAMES: + return _ALLOWED_TYPING_NAMES[node.attr] + raise ValueError("Unsupported annotation attribute") + + if isinstance(node, ast.Subscript): + origin = _resolve_annotation_node(node.value) + args = _resolve_subscript_slice(node.slice) + return origin[args] + + if isinstance(node, ast.Tuple): + return tuple(_resolve_annotation_node(elt) for elt in node.elts) + + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr): + left = _resolve_annotation_node(node.left) + right = _resolve_annotation_node(node.right) + return left | right + + if isinstance(node, ast.Constant) and node.value is None: + return type(None) + + raise ValueError("Unsupported annotation expression") + + +def _resolve_subscript_slice(slice_node: ast.AST): + if isinstance(slice_node, ast.Index): + slice_node = slice_node.value + if isinstance(slice_node, ast.Tuple): + return tuple(_resolve_annotation_node(elt) for elt in slice_node.elts) + return _resolve_annotation_node(slice_node) + + +def resolve_type(annotation: str, *, allow_unsafe_eval: bool = False, extra_globals: Optional[Dict[str, object]] = None): + """ + Resolve a type annotation string into a Python type. + Previously, primitive support for int, float, str, dict, list, set, tuple, bool. + + Args: + annotation (str): The annotation string (e.g., 'int', 'list[int]', 'dict[str, int]'). + + Returns: + type: The corresponding Python type. + + Raises: + ValueError: If the annotation is unsupported or invalid. + """ + python_types = {**vars(typing), **vars(builtins)} + if extra_globals: + python_types.update(extra_globals) + + if annotation in python_types: + return python_types[annotation] + + try: + parsed = ast.parse(annotation, mode="eval") + return _resolve_annotation_node(parsed.body) + except Exception: + if allow_unsafe_eval: + try: + return eval(annotation, python_types) + except Exception as exc: + raise ValueError(f"Unsupported annotation: {annotation}") from exc + + raise ValueError(f"Unsupported annotation: {annotation}") + + +# TODO :: THIS MUST BE EDITED TO HANDLE THINGS +def get_function_annotations_from_source(source_code: str, function_name: str) -> Dict[str, str]: + """ + Parse the source code to extract annotations for a given function name. + + Args: + source_code (str): The Python source code containing the function. + function_name (str): The name of the function to extract annotations for. + + Returns: + Dict[str, str]: A dictionary of argument names to their annotation strings. + + Raises: + ValueError: If the function is not found in the source code. + """ + tree = ast.parse(source_code) + for node in ast.iter_child_nodes(tree): + if isinstance(node, ast.FunctionDef) and node.name == function_name: + annotations = {} + for arg in node.args.args: + if arg.annotation is not None: + annotation_str = ast.unparse(arg.annotation) + annotations[arg.arg] = annotation_str + return annotations + raise ValueError(f"Function '{function_name}' not found in the provided source code.") + + +# NOW json_loads -> ast.literal_eval -> typing.get_origin +def coerce_dict_args_by_annotations( + function_args: JsonDict, + annotations: Dict[str, object], + *, + allow_unsafe_eval: bool = False, + extra_globals: Optional[Dict[str, object]] = None, +) -> dict: + coerced_args = dict(function_args) # Shallow copy + + for arg_name, value in coerced_args.items(): + if arg_name in annotations: + annotation_str = annotations[arg_name] + try: + annotation_value = annotations[arg_name] + if isinstance(annotation_value, str): + arg_type = resolve_type( + annotation_value, + allow_unsafe_eval=allow_unsafe_eval, + extra_globals=extra_globals, + ) + elif isinstance(annotation_value, typing.ForwardRef): + arg_type = resolve_type( + annotation_value.__forward_arg__, + allow_unsafe_eval=allow_unsafe_eval, + extra_globals=extra_globals, + ) + else: + arg_type = annotation_value + + # Always parse strings using literal_eval or json if possible + if isinstance(value, str): + try: + value = json.loads(value) + except json.JSONDecodeError: + try: + value = ast.literal_eval(value) + except (SyntaxError, ValueError) as e: + if arg_type is not str: + raise ValueError(f"Failed to coerce argument '{arg_name}' to {annotation_str}: {e}") + + origin = typing.get_origin(arg_type) + if origin in (list, dict, tuple, set): + # Let the origin (e.g., list) handle coercion + coerced_args[arg_name] = origin(value) + else: + # Coerce simple types (e.g., int, float) + coerced_args[arg_name] = arg_type(value) + + except Exception as e: + raise ValueError(f"Failed to coerce argument '{arg_name}' to {annotation_str}: {e}") + + return coerced_args + + +def get_function_name_and_docstring(source_code: str, name: Optional[str] = None) -> Tuple[str, str]: + """Gets the name and docstring for a given function source code by parsing the AST. + + Args: + source_code: The source code to parse + name: Optional override for the function name + + Returns: + Tuple of (function_name, docstring) + """ + try: + # Parse the source code into an AST + tree = ast.parse(source_code) + + # Find the last function definition + function_def = None + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef): + function_def = node + + if not function_def: + raise LettaToolCreateError("No function definition found in source code") + + # Get the function name + function_name = name if name is not None else function_def.name + + # Get the docstring if it exists + docstring = ast.get_docstring(function_def) + + if not function_name: + raise LettaToolCreateError("Could not determine function name") + + if not docstring: + # For tools with args_json_schema, the docstring is optional + docstring = f"The {function_name} tool" + + return function_name, docstring + + except Exception as e: + import traceback + + traceback.print_exc() + raise LettaToolCreateError(f"Failed to parse function name and docstring: {str(e)}") diff --git a/letta/functions/async_composio_toolset.py b/letta/functions/async_composio_toolset.py new file mode 100644 index 0000000..3094bf5 --- /dev/null +++ b/letta/functions/async_composio_toolset.py @@ -0,0 +1,109 @@ +import json +from typing import Any + +import aiohttp +from composio import ComposioToolSet as BaseComposioToolSet +from composio.exceptions import ( + ApiKeyNotProvidedError, + ComposioSDKError, + ConnectedAccountNotFoundError, + EnumMetadataNotFound, + EnumStringNotFound, +) + + +class AsyncComposioToolSet(BaseComposioToolSet, runtime="letta", description_char_limit=1024): + """ + Async version of ComposioToolSet client for interacting with Composio API + Used to asynchronously hit the execute action endpoint + + https://docs.composio.dev/api-reference/api-reference/v3/tools/post-api-v-3-tools-execute-action + """ + + def __init__(self, api_key: str, entity_id: str, lock: bool = True): + """ + Initialize the AsyncComposioToolSet client + + Args: + api_key (str): Your Composio API key + entity_id (str): Your Composio entity ID + lock (bool): Whether to use locking (default: True) + """ + super().__init__(api_key=api_key, entity_id=entity_id, lock=lock) + + self.headers = { + "Content-Type": "application/json", + "X-API-Key": self._api_key, + } + + async def execute_action( + self, + action: str, + params: dict[str, Any] = {}, + ) -> dict[str, Any]: + """ + Execute an action asynchronously using the Composio API + + Args: + action (str): The name of the action to execute + params (dict[str, Any], optional): Parameters for the action + + Returns: + dict[str, Any]: The API response + + Raises: + ApiKeyNotProvidedError: if the API key is not provided + ComposioSDKError: if a general Composio SDK error occurs + ConnectedAccountNotFoundError: if the connected account is not found + EnumMetadataNotFound: if enum metadata is not found + EnumStringNotFound: if enum string is not found + aiohttp.ClientError: if a network-related error occurs + ValueError: if an error with the parameters or response occurs + """ + API_VERSION = "v3" + endpoint = f"{self._base_url}/{API_VERSION}/tools/execute/{action}" + + json_payload = { + "entity_id": self.entity_id, + "arguments": params or {}, + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post(endpoint, headers=self.headers, json=json_payload) as response: + print(response, response.status, response.reason, response.content) + if response.status == 200: + return await response.json() + else: + error_text = await response.text() + try: + error_json = json.loads(error_text) + error_message = error_json.get("message", error_text) + error_code = error_json.get("code") + + # Handle specific error codes from Composio API + if error_code == 10401 or "API_KEY_NOT_FOUND" in error_message: + raise ApiKeyNotProvidedError() + if ( + "connected account not found" in error_message.lower() + or "no connected account found" in error_message.lower() + ): + raise ConnectedAccountNotFoundError(f"Connected account not found: {error_message}") + if "enum metadata not found" in error_message.lower(): + raise EnumMetadataNotFound(f"Enum metadata not found: {error_message}") + if "enum string not found" in error_message.lower(): + raise EnumStringNotFound(f"Enum string not found: {error_message}") + except json.JSONDecodeError: + error_message = error_text + + # If no specific error was identified, raise a general error + raise ValueError(f"API request failed with status {response.status}: {error_message}") + except aiohttp.ClientError as e: + # Wrap network errors in ComposioSDKError + raise ComposioSDKError(f"Network error when calling Composio API: {str(e)}") + except ValueError: + # Re-raise ValueError (which could be our custom error message or a JSON parsing error) + raise + except Exception as e: + # Catch any other exceptions and wrap them in ComposioSDKError + raise ComposioSDKError(f"Unexpected error when calling Composio API: {str(e)}") diff --git a/letta/functions/composio_helpers.py b/letta/functions/composio_helpers.py new file mode 100644 index 0000000..58f7f62 --- /dev/null +++ b/letta/functions/composio_helpers.py @@ -0,0 +1,96 @@ +import os +from typing import Any, Optional + +from composio.constants import DEFAULT_ENTITY_ID +from composio.exceptions import ( + ApiKeyNotProvidedError, + ComposioSDKError, + ConnectedAccountNotFoundError, + EnumMetadataNotFound, + EnumStringNotFound, +) + +from letta.constants import COMPOSIO_ENTITY_ENV_VAR_KEY +from letta.functions.async_composio_toolset import AsyncComposioToolSet +from letta.utils import run_async_task + + +# TODO: This is kind of hacky, as this is used to search up the action later on composio's side +# TODO: So be very careful changing/removing these pair of functions +def _generate_func_name_from_composio_action(action_name: str) -> str: + """ + Generates the composio function name from the composio action. + + Args: + action_name: The composio action name + + Returns: + function name + """ + return action_name.lower() + + +def generate_composio_action_from_func_name(func_name: str) -> str: + """ + Generates the composio action from the composio function name. + + Args: + func_name: The composio function name + + Returns: + composio action name + """ + return func_name.upper() + + +def generate_composio_tool_wrapper(action_name: str) -> tuple[str, str]: + # Generate func name + func_name = _generate_func_name_from_composio_action(action_name) + + wrapper_function_str = f"""\ +def {func_name}(**kwargs): + raise RuntimeError("Something went wrong - we should never be using the persisted source code for Composio. Please reach out to Letta team") +""" + + # Compile safety check + _assert_code_gen_compilable(wrapper_function_str.strip()) + + return func_name, wrapper_function_str.strip() + + +async def execute_composio_action_async( + action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None +) -> tuple[str, str]: + entity_id = entity_id or os.getenv(COMPOSIO_ENTITY_ENV_VAR_KEY, DEFAULT_ENTITY_ID) + composio_toolset = AsyncComposioToolSet(api_key=api_key, entity_id=entity_id, lock=False) + try: + response = await composio_toolset.execute_action(action=action_name, params=args) + except ApiKeyNotProvidedError as e: + raise RuntimeError(f"API key not provided or invalid for Composio action '{action_name}': {str(e)}") + except ConnectedAccountNotFoundError as e: + raise RuntimeError(f"Connected account not found for Composio action '{action_name}': {str(e)}") + except EnumMetadataNotFound as e: + raise RuntimeError(f"Enum metadata not found for Composio action '{action_name}': {str(e)}") + except EnumStringNotFound as e: + raise RuntimeError(f"Enum string not found for Composio action '{action_name}': {str(e)}") + except ComposioSDKError as e: + raise RuntimeError(f"Composio SDK error while executing action '{action_name}': {str(e)}") + except Exception as e: + print(type(e)) + raise RuntimeError(f"An unexpected error occurred in Composio SDK while executing action '{action_name}': {str(e)}") + + if response.get("error"): + raise RuntimeError(f"Error while executing action '{action_name}': {str(response['error'])}") + + return response.get("data") + + +def execute_composio_action(action_name: str, args: dict, api_key: Optional[str] = None, entity_id: Optional[str] = None) -> Any: + return run_async_task(execute_composio_action_async(action_name, args, api_key, entity_id)) + + +def _assert_code_gen_compilable(code_str): + try: + compile(code_str, "", "exec") + except SyntaxError as e: + print(f"Syntax error in code: {e}") diff --git a/letta/functions/function_sets/base.py b/letta/functions/function_sets/base.py new file mode 100644 index 0000000..56e7918 --- /dev/null +++ b/letta/functions/function_sets/base.py @@ -0,0 +1,527 @@ +from typing import TYPE_CHECKING, List, Literal, Optional + +if TYPE_CHECKING: + from letta.agents.letta_agent import LettaAgent as Agent + from letta.schemas.agent import AgentState + +from letta.constants import CORE_MEMORY_LINE_NUMBER_WARNING + + +def memory( + agent_state: "AgentState", + command: str, + path: Optional[str] = None, + file_text: Optional[str] = None, + description: Optional[str] = None, + old_string: Optional[str] = None, + new_string: Optional[str] = None, + insert_line: Optional[int] = None, + insert_text: Optional[str] = None, + old_path: Optional[str] = None, + new_path: Optional[str] = None, +) -> Optional[str]: + """ + Memory management tool with various sub-commands for memory block operations. + + Args: + command (str): The sub-command to execute. Supported commands: + - "create": Create a new memory block + - "str_replace": Replace text in a memory block + - "insert": Insert text at a specific line in a memory block + - "delete": Delete a memory block + - "rename": Rename a memory block + path (Optional[str]): Path to the memory block (for str_replace, insert, delete) + file_text (Optional[str]): The value to set in the memory block (for create) + description (Optional[str]): The description to set in the memory block (for create, rename) + old_string (Optional[str]): Old text to replace (for str_replace) + new_string (Optional[str]): New text to replace with (for str_replace) + insert_line (Optional[int]): Line number to insert at (for insert) + insert_text (Optional[str]): Text to insert (for insert) + old_path (Optional[str]): Old path for rename operation + new_path (Optional[str]): New path for rename operation + + Returns: + Optional[str]: Success message or error description + + Examples: + # Replace text in a memory block + memory(agent_state, "str_replace", path="/memories/user_preferences", old_string="theme: dark", new_string="theme: light") + + # Insert text at line 5 + memory(agent_state, "insert", path="/memories/notes", insert_line=5, insert_text="New note here") + + # Delete a memory block + memory(agent_state, "delete", path="/memories/old_notes") + + # Rename a memory block + memory(agent_state, "rename", old_path="/memories/temp", new_path="/memories/permanent") + + # Update the description of a memory block + memory(agent_state, "rename", path="/memories/temp", description="The user's temporary notes.") + + # Create a memory block with starting text + memory(agent_state, "create", path="/memories/coding_preferences", "description": "The user's coding preferences.", "file_text": "The user seems to add type hints to all of their Python code.") + + # Create an empty memory block + memory(agent_state, "create", path="/memories/coding_preferences", "description": "The user's coding preferences.") + """ + raise NotImplementedError("This should never be invoked directly. Contact Letta if you see this error message.") + + +def send_message(self: "Agent", message: str) -> Optional[str]: + """ + Sends a message to the human user. + + Args: + message (str): Message contents. All unicode (including emojis) are supported. + + Returns: + Optional[str]: None is always returned as this function does not produce a response. + """ + # FIXME passing of msg_obj here is a hack, unclear if guaranteed to be the correct reference + if self.interface: + self.interface.assistant_message(message) # , msg_obj=self._messages[-1]) + return None + + +def conversation_search( + self: "Agent", + query: Optional[str] = None, + roles: Optional[List[Literal["assistant", "user", "tool"]]] = None, + limit: Optional[int] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, +) -> Optional[str]: + """ + Search prior conversation history using hybrid search (text + semantic similarity). + + Args: + query (Optional[str]): String to search for using both text matching and semantic similarity. If not provided, returns messages based on other filters (time range, roles). + roles (Optional[List[Literal["assistant", "user", "tool"]]]): Optional list of message roles to filter by. + limit (Optional[int]): Maximum number of results to return. Uses system default if not specified. + start_date (Optional[str]): Filter results to messages created on or after this date (INCLUSIVE). When using date-only format (e.g., "2024-01-15"), includes messages starting from 00:00:00 of that day. ISO 8601 format: "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM". Examples: "2024-01-15" (from start of Jan 15), "2024-01-15T14:30" (from 2:30 PM on Jan 15). + end_date (Optional[str]): Filter results to messages created on or before this date (INCLUSIVE). When using date-only format (e.g., "2024-01-20"), includes all messages from that entire day. ISO 8601 format: "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM". Examples: "2024-01-20" (includes all of Jan 20), "2024-01-20T17:00" (up to 5 PM on Jan 20). + + Examples: + # Search all messages + conversation_search(query="project updates") + + # Search only assistant messages + conversation_search(query="error handling", roles=["assistant"]) + + # Search with date range (inclusive of both dates) + conversation_search(query="meetings", start_date="2024-01-15", end_date="2024-01-20") + # This includes all messages from Jan 15 00:00:00 through Jan 20 23:59:59 + + # Search messages from a specific day (inclusive) + conversation_search(query="bug reports", start_date="2024-09-04", end_date="2024-09-04") + # This includes ALL messages from September 4, 2024 + + # Search with specific time boundaries + conversation_search(query="deployment", start_date="2024-01-15T09:00", end_date="2024-01-15T17:30") + # This includes messages from 9 AM to 5:30 PM on Jan 15 + + # Search with limit + conversation_search(query="debugging", limit=10) + + # Time-range only search (no query) + conversation_search(start_date="2024-01-15", end_date="2024-01-20") + # Returns all messages from Jan 15 through Jan 20 + + Returns: + str: Query result string containing matching messages with timestamps and content. + """ + + from letta.constants import RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE + from letta.helpers.json_helpers import json_dumps + + # Use provided limit or default + if limit is None: + limit = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE + + messages = self.message_manager.list_messages_for_agent( + agent_id=self.agent_state.id, + actor=self.user, + query_text=query, + roles=roles, + limit=limit, + ) + + if len(messages) == 0: + results_str = "No results found." + else: + results_pref = f"Found {len(messages)} results:" + results_formatted = [] + for message in messages: + # Extract text content from message + text_content = message.content[0].text if message.content else "" + result_entry = {"role": message.role, "content": text_content} + results_formatted.append(result_entry) + results_str = f"{results_pref} {json_dumps(results_formatted)}" + return results_str + + +async def archival_memory_insert(self: "Agent", content: str, tags: Optional[list[str]] = None) -> Optional[str]: + """ + Add information to long-term archival memory for later retrieval. + + Use this tool to store facts, knowledge, or context that you want to remember + across all future conversations. Archival memory is permanent and searchable by + semantic similarity. + + Best practices: + - Store self-contained facts or summaries, not conversational fragments + - Add descriptive tags to make information easier to find later + - Use for: meeting notes, project updates, conversation summaries, events, reports + - Information stored here persists indefinitely and can be searched semantically + + Args: + content: The information to store. Should be clear and self-contained. + tags: Optional list of category tags (e.g., ["meetings", "project-updates"]) + + Returns: + Confirmation message with the ID of the inserted memory. + + Examples: + archival_memory_insert( + content="Meeting on 2024-03-15: Discussed Q2 roadmap priorities. Decided to focus on performance optimization and API v2 release. John will lead the optimization effort.", + tags=["meetings", "roadmap", "q2-2024"] + ) + """ + raise NotImplementedError("This should never be invoked directly. Contact Letta if you see this error message.") + + +async def archival_memory_search( + self: "Agent", + query: str, + tags: Optional[list[str]] = None, + tag_match_mode: Literal["any", "all"] = "any", + top_k: Optional[int] = None, + start_datetime: Optional[str] = None, + end_datetime: Optional[str] = None, +) -> Optional[str]: + """ + Search archival memory using semantic similarity to find relevant information. + + This tool searches your long-term memory storage by meaning, not exact keyword + matching. Use it when you need to recall information from past conversations or + knowledge you've stored. + + Search strategy: + - Query by concept/meaning, not exact phrases + - Use tags to narrow results when you know the category + - Start broad, then narrow with tags if needed + - Results are ranked by semantic relevance + + Args: + query: What you're looking for, described naturally (e.g., "meetings about API redesign") + tags: Filter to memories with these tags. Use tag_match_mode to control matching. + tag_match_mode: "any" = match memories with ANY of the tags, "all" = match only memories with ALL tags + start_datetime: Only return memories created after this time (ISO 8601: "2024-01-15" or "2024-01-15T14:30") + end_datetime: Only return memories created before this time (ISO 8601 format) + top_k: Maximum number of results to return (default: 10) + + Returns: + A list of relevant memories with IDs, timestamps, and content, ranked by similarity. + + Examples: + # Search for project discussions + archival_memory_search( + query="database migration decisions and timeline", + tags=["projects"] + ) + + # Search meeting notes from Q1 + archival_memory_search( + query="roadmap planning discussions", + start_datetime="2024-01-01", + end_datetime="2024-03-31", + tags=["meetings", "roadmap"], + tag_match_mode="all" + ) + """ + raise NotImplementedError("This should never be invoked directly. Contact Letta if you see this error message.") + + +def core_memory_append(agent_state: "AgentState", label: str, content: str) -> str: # type: ignore + """ + Append to the contents of core memory. + + Args: + label (str): Section of the memory to be edited. + content (str): Content to write to the memory. All unicode (including emojis) are supported. + + Returns: + str: The updated value of the memory block. + """ + current_value = str(agent_state.memory.get_block(label).value) + new_value = current_value + "\n" + str(content) + agent_state.memory.update_block_value(label=label, value=new_value) + return new_value + + +def core_memory_replace(agent_state: "AgentState", label: str, old_content: str, new_content: str) -> str: # type: ignore + """ + Replace the contents of core memory. To delete memories, use an empty string for new_content. + + Args: + label (str): Section of the memory to be edited. + old_content (str): String to replace. Must be an exact match. + new_content (str): Content to write to the memory. All unicode (including emojis) are supported. + + Returns: + str: The updated value of the memory block. + """ + current_value = str(agent_state.memory.get_block(label).value) + if old_content not in current_value: + raise ValueError(f"Old content '{old_content}' not found in memory block '{label}'") + new_value = current_value.replace(str(old_content), str(new_content)) + agent_state.memory.update_block_value(label=label, value=new_value) + return new_value + + +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: + from letta.schemas.block import Block + + new_block = Block(label=target_block_label, value=new_memory) + agent_state.memory.set_block(new_block) + + agent_state.memory.update_block_value(label=target_block_label, value=new_memory) + return None + + +## Attempted v2 of sleep-time function set, meant to work better across all types + +SNIPPET_LINES: int = 4 + + +# Based off of: https://github.com/anthropics/anthropic-quickstarts/blob/main/computer-use-demo/computer_use_demo/tools/edit.py?ref=musings.yasyf.com#L154 +def memory_replace(agent_state: "AgentState", label: str, old_string: str, new_string: str) -> str: # type: ignore + """ + The memory_replace command allows you to replace a specific string in a memory block with a new string. This is used for making precise edits. + Do NOT attempt to replace long strings, e.g. do not attempt to replace the entire contents of a memory block with a new string. + + Args: + label (str): Section of the memory to be edited, identified by its label. + old_string (str): The text to replace (must match exactly, including whitespace and indentation). + new_string (str): The new text to insert in place of the old text. Do not include line number prefixes. + + Examples: + # Update a block containing information about the user + memory_replace(label="human", old_string="Their name is Alice", new_string="Their name is Bob") + + # Update a block containing a todo list + memory_replace(label="todos", old_string="- [ ] Step 5: Search the web", new_string="- [x] Step 5: Search the web") + + # Pass an empty string to + memory_replace(label="human", old_string="Their name is Alice", new_string="") + + # Bad example - do NOT add (view-only) line numbers to the args + memory_replace(label="human", old_string="1: Their name is Alice", new_string="1: Their name is Bob") + + # Bad example - do NOT include the line number warning either + memory_replace(label="human", old_string="# NOTE: Line numbers shown below (with arrows like '1→') are to help during editing. Do NOT include line number prefixes in your memory edit tool calls.\\n1→ Their name is Alice", new_string="1→ Their name is Bob") + + # Good example - no line numbers or line number warning (they are view-only), just the text + memory_replace(label="human", old_string="Their name is Alice", new_string="Their name is Bob") + + Returns: + str: The updated value of the memory block. + """ + import re + + if bool(re.search(r"\nLine \d+: ", old_string)): + raise ValueError( + "old_string contains a line number prefix, which is not allowed. Do not include line numbers when calling memory tools (line numbers are for display purposes only)." + ) + if CORE_MEMORY_LINE_NUMBER_WARNING in old_string: + raise ValueError( + "old_string contains a line number warning, which is not allowed. Do not include line number information when calling memory tools (line numbers are for display purposes only)." + ) + if bool(re.search(r"\nLine \d+: ", new_string)): + raise ValueError( + "new_string contains a line number prefix, which is not allowed. Do not include line numbers when calling memory tools (line numbers are for display purposes only)." + ) + + old_string = str(old_string).expandtabs() + new_string = str(new_string).expandtabs() + current_value = str(agent_state.memory.get_block(label).value).expandtabs() + + # Check if old_string is unique in the block + occurences = current_value.count(old_string) + if occurences == 0: + raise ValueError( + f"No replacement was performed, old_string `{old_string}` did not appear verbatim in memory block with label `{label}`." + ) + elif occurences > 1: + content_value_lines = current_value.split("\n") + lines = [idx + 1 for idx, line in enumerate(content_value_lines) if old_string in line] + raise ValueError( + f"No replacement was performed. Multiple occurrences of old_string `{old_string}` in lines {lines}. Please ensure it is unique." + ) + + # Replace old_string with new_string + new_value = current_value.replace(str(old_string), str(new_string)) + + # Write the new content to the block + agent_state.memory.update_block_value(label=label, value=new_value) + + # Create a snippet of the edited section + # SNIPPET_LINES = 3 + # replacement_line = current_value.split(old_string)[0].count("\n") + # start_line = max(0, replacement_line - SNIPPET_LINES) + # end_line = replacement_line + SNIPPET_LINES + new_string.count("\n") + # snippet = "\n".join(new_value.split("\n")[start_line : end_line + 1]) + + return new_value + + +def memory_insert(agent_state: "AgentState", label: str, new_string: str, insert_line: int = -1) -> str: # type: ignore + """ + The memory_insert command allows you to insert text at a specific location in a memory block. + + Args: + label (str): Section of the memory to be edited, identified by its label. + new_string (str): The text to insert. Do not include line number prefixes. + insert_line (int): The line number after which to insert the text (0 for beginning of file). Defaults to -1 (end of the file). + + Examples: + # Update a block containing information about the user (append to the end of the block) + memory_insert(label="customer", new_string="The customer's ticket number is 12345") + + # Update a block containing information about the user (insert at the beginning of the block) + memory_insert(label="customer", new_string="The customer's ticket number is 12345", insert_line=0) + + Returns: + Optional[str]: None is always returned as this function does not produce a response. + """ + import re + + if bool(re.search(r"\nLine \d+: ", new_string)): + raise ValueError( + "new_string contains a line number prefix, which is not allowed. Do not include line numbers when calling memory tools (line numbers are for display purposes only)." + ) + if CORE_MEMORY_LINE_NUMBER_WARNING in new_string: + raise ValueError( + "new_string contains a line number warning, which is not allowed. Do not include line number information when calling memory tools (line numbers are for display purposes only)." + ) + + current_value = str(agent_state.memory.get_block(label).value).expandtabs() + new_string = str(new_string).expandtabs() + current_value_lines = current_value.split("\n") + n_lines = len(current_value_lines) + + # Check if we're in range, from 0 (pre-line), to 1 (first line), to n_lines (last line) + if insert_line == -1: + insert_line = n_lines + elif insert_line < 0 or insert_line > n_lines: + raise ValueError( + f"Invalid `insert_line` parameter: {insert_line}. It should be within the range of lines of the memory block: {[0, n_lines]}, or -1 to append to the end of the memory block." + ) + + # Insert the new string as a line + new_string_lines = new_string.split("\n") + new_value_lines = current_value_lines[:insert_line] + new_string_lines + current_value_lines[insert_line:] + ( + current_value_lines[max(0, insert_line - SNIPPET_LINES) : insert_line] + + new_string_lines + + current_value_lines[insert_line : insert_line + SNIPPET_LINES] + ) + + # Collate into the new value to update + new_value = "\n".join(new_value_lines) + # snippet = "\n".join(snippet_lines) + + # Write into the block + agent_state.memory.update_block_value(label=label, value=new_value) + + return new_value + + +def memory_apply_patch(agent_state: "AgentState", label: str, patch: str) -> str: # type: ignore + """ + Apply a simplified unified-diff style patch to one or more memory blocks. + + Backwards compatible behavior: + - If `patch` contains no "***" headers, it applies the patch to the single memory block + identified by `label`. + + Extended, codex-style behavior (multi-block): + - `*** Add Block: