From 73565f76a3fa1c5e2afb689ce276568af7d0801e Mon Sep 17 00:00:00 2001 From: Letta Integration <300689746+letta-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:31:30 +0000 Subject: [PATCH 01/13] feat(agents): add Letta Code local backend A2A wrapper --- src/agent_env/agents/README.md | 8 + .../agents/agents/letta-code/.dockerignore | 3 + .../agents/agents/letta-code/Dockerfile | 37 + .../agents/agents/letta-code/README.md | 38 + .../agents/agents/letta-code/a2a_server.py | 637 ++ .../agents/agents/letta-code/entrypoint.sh | 4 + .../agents/agents/letta-code/install.sh | 175 + .../agents/letta-code/letta_code_bridge.py | 498 ++ .../agents/letta-code/letta_code_kit.py | 75 + .../agents/letta-code/package-lock.json | 5447 +++++++++++++++++ .../agents/agents/letta-code/package.json | 6 + .../agents/agents/letta-code/requirements.txt | 6 + src/agent_env/agents/agents/letta-code/run.sh | 12 + src/agent_env/agents/aliases.toml | 1 + .../agents/tasks/letta-code-smoke.json | 7 + tst/unit/agents/letta_code/agent.py | 20 + 16 files changed, 6974 insertions(+) create mode 100644 src/agent_env/agents/agents/letta-code/.dockerignore create mode 100644 src/agent_env/agents/agents/letta-code/Dockerfile create mode 100644 src/agent_env/agents/agents/letta-code/README.md create mode 100644 src/agent_env/agents/agents/letta-code/a2a_server.py create mode 100644 src/agent_env/agents/agents/letta-code/entrypoint.sh create mode 100644 src/agent_env/agents/agents/letta-code/install.sh create mode 100644 src/agent_env/agents/agents/letta-code/letta_code_bridge.py create mode 100644 src/agent_env/agents/agents/letta-code/letta_code_kit.py create mode 100644 src/agent_env/agents/agents/letta-code/package-lock.json create mode 100644 src/agent_env/agents/agents/letta-code/package.json create mode 100644 src/agent_env/agents/agents/letta-code/requirements.txt create mode 100644 src/agent_env/agents/agents/letta-code/run.sh create mode 100644 src/agent_env/agents/tasks/letta-code-smoke.json create mode 100644 tst/unit/agents/letta_code/agent.py diff --git a/src/agent_env/agents/README.md b/src/agent_env/agents/README.md index d9a8cd422..720aa4bf0 100644 --- a/src/agent_env/agents/README.md +++ b/src/agent_env/agents/README.md @@ -61,6 +61,14 @@ OpenAI-compatible one, `/v1/chat/completions`, and takes its model from th `anthropic/claude-sonnet-4-6`. It declares `install/v1` too, into a container or onto a `vm` sandbox's Linux host, with glibc. Its folder's `README.md` covers the extensions it implements. +`agents/letta-code/` runs [Letta Code](https://github.com/letta-ai/letta-code) behind the protocol's A2A agent +framework: each turn is one `letta --backend local -p --output-format stream-json` run, resumed in the context's +conversation, with a separate local agent and memory per context. Name it as `letta-code`, its alias, or by its id, +`@local/agentenv-framework/agents/letta-code`. It calls the `[model]` endpoint as an OpenAI-compatible one through +a relay that adds task attribution, and takes its model from the prompt, else `openai/gpt-4.1-mini`. It declares +`install/v1` too, into a container or onto a Linux host with glibc. Its folder's `README.md` covers the extensions +it implements. Run its smoke task with `agent-env run agents --task letta-code-smoke`. + `agents/openhands/` runs the [OpenHands](https://github.com/OpenHands/software-agent-sdk) agent behind the protocol's A2A agent framework, driving the OpenHands SDK in process rather than a CLI: each turn is one run of a persisted SDK conversation, the task env's MCP servers go to its `mcp_config`, and its SDK events are the turn's diff --git a/src/agent_env/agents/agents/letta-code/.dockerignore b/src/agent_env/agents/agents/letta-code/.dockerignore new file mode 100644 index 000000000..513f73947 --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/.dockerignore @@ -0,0 +1,3 @@ +node_modules +__pycache__ +*.pyc diff --git a/src/agent_env/agents/agents/letta-code/Dockerfile b/src/agent_env/agents/agents/letta-code/Dockerfile new file mode 100644 index 000000000..0c12e79c9 --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/Dockerfile @@ -0,0 +1,37 @@ +FROM public.ecr.aws/docker/library/python:3.11-slim@sha256:c20888b6acdd1e63e1c433a185bf3ad162c0288fe484616ce062e0d28add2900 +# Node to install the Letta Code CLI, a native binary, and for the model's commands, checked against its published +# checksum; install.sh brings the same. poppler-utils renders a PDF attachment to the page images the model reads. +ARG NODE_VERSION=22.23.3 +ARG NODE_SHA256_X64=1084aa36196bba4c3a5e69a1ee388a6e4ff729dad09445fbcd434b28fe3c24af +ARG NODE_SHA256_ARM64=5ced2d48d1d7198739b7f86804de0171aefb6823b684b12341d3321afc3cb0b2 +RUN set -eu; \ + apt-get update && apt-get install -y --no-install-recommends ca-certificates curl poppler-utils git \ + && rm -rf /var/lib/apt/lists/*; \ + case "$(uname -m)" in x86_64) cpu=x64 sha256=$NODE_SHA256_X64 ;; aarch64) cpu=arm64 sha256=$NODE_SHA256_ARM64 ;; \ + *) echo "Node has no build for $(uname -m)" >&2; exit 1 ;; esac; \ + curl -fsSL --retry 3 "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${cpu}.tar.gz" \ + -o /tmp/node.tar.gz; \ + echo "$sha256 /tmp/node.tar.gz" | sha256sum -c -; \ + tar -xzf /tmp/node.tar.gz -C /usr/local --strip-components=1 --no-same-owner; \ + rm /tmp/node.tar.gz +WORKDIR /opt/agent +# node-pty is interactive-only and has no Linux prebuild. Headless needs only these postinstalls. +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts --omit=dev --no-audit --no-fund --loglevel=error \ + && npm rebuild @letta-ai/letta-code sharp protobufjs \ + && npm cache clean --force \ + && ln -s /opt/agent/node_modules/.bin/letta /usr/local/bin/letta +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt +# install.sh and run.sh are install/v1's: in the image, so they stay in its build context. +COPY a2a_server.py letta_code_bridge.py letta_code_kit.py entrypoint.sh install.sh run.sh ./ +# The CLI's cwd, /app, and its state under /data, fixed so neither follows the uid: Modal runs the image as root. +RUN useradd --create-home --uid 10001 agent && mkdir -p /app /data \ + && chown agent:agent /app /data && chmod 0755 entrypoint.sh && chmod -R go+rX /opt/agent +ENV A2A_PORT=8000 PYTHONPATH=/opt/agent HOME=/data XDG_CONFIG_HOME=/data/config XDG_CACHE_HOME=/data/cache \ + XDG_DATA_HOME=/data/share XDG_STATE_HOME=/data/state +USER agent +WORKDIR /app +EXPOSE 8000 +# CMD rather than ENTRYPOINT: Modal passes a sandbox's env to the image's CMD only. +CMD ["/opt/agent/entrypoint.sh"] diff --git a/src/agent_env/agents/agents/letta-code/README.md b/src/agent_env/agents/agents/letta-code/README.md new file mode 100644 index 000000000..e295c5de3 --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/README.md @@ -0,0 +1,38 @@ +# Letta Code A2A agent + +The [Letta Code](https://github.com/letta-ai/letta-code) CLI wrapped with `agentenv_protocol.a2a_agent`, shipped +in the `agents` bundle as `@local/agentenv-framework/agents/letta-code`, with alias `letta-code`. This directory +is the whole build context. The Dockerfile pins the Python base by digest and Node by version and checksum; +`package-lock.json` pins `@letta-ai/letta-code` to 0.34.25. + +Each turn runs `letta --backend local -p --output-format stream-json --yolo`. The bridge creates an agent with +`letta agents create`; the first turn uses `--agent --new`, and subsequent turns use +`--agent --conversation `. Each context has a separate home under `$HOME/sessions/`. Different contexts +run concurrently; turns in one context and snapshot operations are synchronized. + +The bridge connects the local backend with `letta connect openai-compatible`. A per-turn loopback relay forwards +requests to `$LITELLM_BASE_URL/v1`, with `LITELLM_API_KEY`, `[model.headers]` (`LITELLM_EXTRA_HEADERS`), and +`project_id` / `task_id` as `x-litellm-customer-id` and `x-litellm-tags`. The model defaults to +`openai/gpt-4.1-mini`. Endpoint credentials stay at the relay. Registered MCP servers retain their names and headers; +a task role replaces `AgentEnv-Role`. Task instructions explain discovery and the full `mcp____` +call form for the CLI's MCP tools, including peer tools. + +| Extension | Implementation | +|---|---| +| `agent-config/v1` | SDK set/get; `model`, `effort`, `system_prompt`, `output_format`, `project_id`, `task_id`, `timeout_seconds`. Before every turn, task instructions are committed to the existing agent's core memory, which the local backend compiles into its system prompt. JSON schema answers are validated locally. | +| `mcp-config/v1` | SDK registration and listing; servers passed to the context's agent settings each turn, with exact-name discovery instructions. | +| `skill-config/v1` | Inline and bundle skills installed under `$HOME/.letta/skills/`, copied into each context's home before its turn. | +| `trajectory/v1` | Native stream-json events appended live, format `letta-code-events/1`. | +| `snapshot/v1` | Save archives the complete context home, including conversations, agent records, settings and git-backed memory; load stages it and restores it into the target context, rewriting stored home paths and binding the saved conversation. Optional workspace archives restore `/app`. GNU tar changelog captures task-level file increments, including removed roots, using the OpenCode template's implementation. | +| `peer-agents/v1` | SDK default; its `peer_agents` MCP server is registered alongside task servers. | +| `attribution-probe/v1` | Reports the project and task attribution used by the last CLI turn. | +| `install/v1` | Template installer brings checksum-verified Node and uv, the pinned CLI, and a private Python into a container or a Linux host with glibc. Installs git if missing. | +| `staging/v1` | SDK default. | + +The image runs as user `agent` in `/app`, with state under `/data`, and includes git and `pdftoppm`. PDF attachments +are rendered as page images; other attachments are materialized and their paths passed to the CLI. Headless mode +never loads node-pty, so installation skips dependency scripts and rebuilds only the CLI, sharp and protobufjs. +The end of CLI stderr is included in failures. Cancellation and shutdown kill the CLI's process tree. + +Run `agent-env run agents --task letta-code-smoke` for the shell smoke task and +`agent-env a2a-agent validate --id letta-code --sandbox local` for the repository's validator. diff --git a/src/agent_env/agents/agents/letta-code/a2a_server.py b/src/agent_env/agents/agents/letta-code/a2a_server.py new file mode 100644 index 000000000..eb214936a --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/a2a_server.py @@ -0,0 +1,637 @@ +"""A2A agent wrapping the Letta Code CLI. + +Built on agentenv-protocol's `AgentEnvAgent`: the framework serves the Agent Card, +the A2A task lifecycle and the SDK-owned extensions (agent-config, mcp-config, +skill listing, task trajectories, peer agents). Each turn is one +`letta -p --output-format stream-json`, whose events are appended to the framework's +trajectory log as the CLI prints them. A context's first turn starts a Letta Code +session, which later turns resume with `--conversation `. +""" +from __future__ import annotations + +import asyncio +import base64 +import binascii +import json +import logging +import os +import re +import shutil +import subprocess +import tempfile +import uuid +from contextlib import asynccontextmanager +from pathlib import Path +from typing import Any +from urllib.parse import urlparse +from weakref import WeakValueDictionary + +import httpx +from a2a.types import AgentSkill +from agentenv_protocol.a2a_agent import ( + ATTRIBUTION_PROBE_V1, + INSTALL_V1, + MCP_CONFIG_V1, + PEER_AGENTS_V1, + SKILL_CONFIG_V1, + SNAPSHOT_V1, + TRAJECTORY_V1, + AgentConfig, + AgentEnvAgent, + AgentIdentity, + AttributionProbeResponse, + BundleSkillRequest, + DataPart, + FilePart, + InlineSkillRequest, + NamespaceChangelogEnableRequest, + NamespaceUploader, + ObjectChangelogApplyRequest, + ObjectSnapshotLoadRequest, + ObjectSnapshotSaveRequest, + SkillAddResponse, + TaskRequest, + TaskResult, + TextPart, + Usage, + a2a_agent, + download, + enable, + extension, + upload, +) +from pydantic import Field, field_validator +from starlette.applications import Starlette +from starlette.exceptions import HTTPException + +from letta_code_bridge import ( + ERROR_MESSAGE_LIMIT, + event_session_id, + export_session, + get_last_attribution_snapshot, + import_session, + kill_running_clis, + run_letta_code, + validate_output_format, +) +from letta_code_kit import DurableSessionRefs, install_skill, json_error_bodies + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +_XDG_CONFIG = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config")) +SKILLS_DIR = Path.home() / ".letta" / "skills" +WORKSPACE_DIR = Path(os.environ.get("LETTA_CODE_WORKSPACE_DIR", "/app")) +INPUTS_DIR = WORKSPACE_DIR / "inputs" +# One per port, since agents installed on one host listen on their own. +_CHANGELOG_DIR = Path(tempfile.gettempdir()) / f"letta_code-changelog-{os.environ.get('A2A_PORT') or 'default'}" +_CHANGELOG_SNAR = _CHANGELOG_DIR / "state.snar" +# Each increment's record of the roots it saw removed, in a directory of its own beside the changelog, which captures +# exclude, and outside its roots: tar dumps a directory it hasn't seen whole, so the record goes in however soon after +# the last capture it is written. +_META_DIR_SUFFIX = ".changelog-meta" +_META_NAME = "letta_code-changelog-meta.json" +_CHANGELOG_META_DIR = _CHANGELOG_DIR.parent / f"{_CHANGELOG_DIR.name}{_META_DIR_SUFFIX}" +# A record in an increment: _META_NAME in a sequence's directory under a directory named with _META_DIR_SUFFIX, +# wherever the capturing agent kept it; a file of that name anywhere else is a file of the layer. +_META_MEMBER = re.compile(rf"(?:^|/)[^/]+{re.escape(_META_DIR_SUFFIX)}/\d+/{re.escape(_META_NAME)}\Z") +# Never removed on replay, whatever an increment's record says. +_SYSTEM_DIRS = frozenset({"/", "/bin", "/boot", "/dev", "/etc", "/home", "/lib", "/lib32", "/lib64", "/libx32", + "/media", "/mnt", "/opt", "/proc", "/root", "/run", "/sbin", "/srv", "/sys", "/tmp", "/usr", + "/var"}) +_TAR_VERSION_TIMEOUT_S = 10 +_FILE_DOWNLOAD_TIMEOUT_S = 60 +_PDF_RENDER_TIMEOUT_S = 60 +_PDF_RENDER_DPI = 150 +_SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9._-]") +# The versioned format of the `letta -p --output-format stream-json` events this agent appends to its trajectory. +TRAJECTORY_FORMAT = "letta-code-events/1" + + +def _backup_exclusions() -> list[str]: + if not WORKSPACE_DIR.exists(): + return [] + return [ + f"--exclude=./{path.name}" + for path in WORKSPACE_DIR.iterdir() + if path.is_dir() + and path.name.startswith(".letta_code-backup-") + and (path / "saved").is_dir() + ] + + +def _changelog_backup_exclusions() -> list[str]: + root = WORKSPACE_DIR.relative_to("/") + exclusions = _backup_exclusions() + return exclusions + [ + f"--exclude={root / option.removeprefix('--exclude=./')}" + for option in exclusions + ] + + +class LettaCodeConfig(AgentConfig): + model: str | None = None + effort: str | None = None + system_prompt: str | None = None + output_format: dict[str, Any] | None = None + project_id: str | None = None + task_id: str | None = None + timeout_seconds: int = Field(default=1800, gt=0) + + @field_validator("output_format") + @classmethod + def check_output_format(cls, value: dict[str, Any] | None) -> dict[str, Any] | None: + if value is not None: + validate_output_format(value) + return value + + +class FilePartMaterializeError(Exception): + """A supplied file cannot be passed to Letta Code.""" + + +def _safe_name(candidate: str | None, fallback_ext: str = "") -> str: + if candidate: + candidate = _SAFE_NAME_RE.sub("_", Path(candidate).name) + return candidate or f"{uuid.uuid4().hex}{fallback_ext}" + + +def _download_uri_to(uri: str, target: Path) -> None: + parsed = urlparse(uri) + if parsed.scheme in ("http", "https"): + try: + with httpx.Client(timeout=_FILE_DOWNLOAD_TIMEOUT_S, follow_redirects=True) as client: + response = client.get(uri) + response.raise_for_status() + target.write_bytes(response.content) + except httpx.HTTPError as exc: + raise FilePartMaterializeError("HTTP file download failed") from exc + else: + raise FilePartMaterializeError(f"unsupported file URI scheme {parsed.scheme!r}") + + +def _materialize_and_render(part: FilePart, task_id: str) -> list[str]: + directory = INPUTS_DIR / task_id + directory.mkdir(parents=True, exist_ok=True) + name = _safe_name(part.name) + target = directory / name + if target.exists(): + target = directory / f"{uuid.uuid4().hex[:8]}-{name}" + if part.bytes is not None: + try: + target.write_bytes(base64.b64decode(part.bytes, validate=True)) + except (binascii.Error, ValueError, OSError) as exc: + raise FilePartMaterializeError(f"invalid file attachment {name!r}") from exc + else: + _download_uri_to(part.uri, target) + if (part.mime_type or "").lower() != "application/pdf" and target.suffix.lower() != ".pdf": + return [str(target)] + # The openai-compatible provider can't forward PDF document blocks, so a PDF goes as its pages' images. + prefix = str(target.with_suffix("")) + "_page" + try: + subprocess.run( + ["pdftoppm", "-png", "-r", str(_PDF_RENDER_DPI), str(target), prefix], + check=True, capture_output=True, timeout=_PDF_RENDER_TIMEOUT_S, + ) + except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): + logger.warning("PDF rendering failed for %s; passing the PDF directly", target) + return [str(target)] + pages = sorted(directory.glob(f"{Path(prefix).name}*.png")) + return [str(page) for page in pages] or [str(target)] + + +class _WorkspaceBarrier: + """Turns share the workspace and snapshots take it alone, so a snapshot's session and files come from a point no + turn is writing at, while turns of different contexts run at once. A waiting snapshot holds new turns back, so + it is never starved.""" + + def __init__(self) -> None: + self._condition = asyncio.Condition() + self._readers = 0 + self._writer = False + self._waiting_writers = 0 + + @asynccontextmanager + async def shared(self): + async with self._condition: + await self._condition.wait_for(lambda: not self._writer and self._waiting_writers == 0) + self._readers += 1 + try: + yield + finally: + async with self._condition: + self._readers -= 1 + if self._readers == 0: + self._condition.notify_all() + + @asynccontextmanager + async def exclusive(self): + async with self._condition: + self._waiting_writers += 1 + try: + await self._condition.wait_for(lambda: not self._writer and self._readers == 0) + self._writer = True + finally: + self._waiting_writers -= 1 + if not self._writer: + self._condition.notify_all() + try: + yield + finally: + async with self._condition: + self._writer = False + self._condition.notify_all() + + +@asynccontextmanager +async def lifespan(app: Starlette): + """As the server stops, the CLIs of turns still running are killed, which would otherwise go on in the workspace + after it.""" + try: + yield + finally: + if stopped := kill_running_clis(): + logger.info(f"Killed {stopped} letta_code CLI(s) still running a turn as the agent shut down") + + +# install.sh, from the build context install_agent unpacks, brings Node, the Letta Code CLI and a Python of the agent's +# own; the endpoint reaches it as environment, never as arguments. +_UNPACK = "mkdir -p {work_dir}/agent && tar -xzf {agent_ctx_tar} -C {work_dir}/agent" +_ENDPOINT = ("LITELLM_API_KEY={litellm_api_key} LITELLM_BASE_URL={litellm_base_url} " + "LITELLM_EXTRA_HEADERS={litellm_extra_headers} AGENT_TRUST_ENV={trust_env} ") +_ENDPOINT_PARAMS = ["litellm_api_key", "litellm_base_url", "litellm_extra_headers", "trust_env"] +_INSTALL_COMMANDS = [_UNPACK, _ENDPOINT + "bash {work_dir}/agent/install.sh container {container} {a2a_port}"] +_HOST_WORKDIR = "/app" +_INSTALL_COMMANDS_HOST = [_UNPACK, _ENDPOINT + f"bash {{work_dir}}/agent/install.sh host {{agent_name}} {{a2a_port}} {_HOST_WORKDIR}"] + + +@a2a_agent( + identity=AgentIdentity( + name="Letta Code", + description="A2A-compliant wrapper around the Letta Code CLI.", + version="1.0.0", + input_modes=("text", "image/png", "image/jpeg", "image/gif", "image/webp", "application/pdf"), + output_modes=("text",), + skills=(AgentSkill( + id="letta-code", name="letta-code", + description="Runs the Letta Code CLI with full tool access (bash, read, edit, glob, grep).", + tags=["coding", "agent"], + ),), + ), + config=LettaCodeConfig, + extensions=( + MCP_CONFIG_V1, + enable(TRAJECTORY_V1, live=True), + enable(ATTRIBUTION_PROBE_V1), + enable( + INSTALL_V1, + install_commands=_INSTALL_COMMANDS, + # Port the agent's A2A server listens on inside the container. + a2a_port=8000, + # Every placeholder referenced in install_commands must be in this list. + required_params=["container", "work_dir", "agent_ctx_tar", "a2a_port", *_ENDPOINT_PARAMS], + install_commands_host=_INSTALL_COMMANDS_HOST, + required_params_host=["work_dir", "agent_ctx_tar", "a2a_port", "agent_name", *_ENDPOINT_PARAMS], + workdir_host=_HOST_WORKDIR, + user_host="root", + ), + # SDK default: the CLI reaches the task's other agents through the SDK's peer_agents MCP tools. + PEER_AGENTS_V1, + ), + lifespan=lifespan, + workspace=str(WORKSPACE_DIR), +) +class LettaCodeAgent(DurableSessionRefs, AgentEnvAgent): + """Bridges A2A tasks onto `letta_code run`.""" + + def __init__(self) -> None: + self._turn_locks: WeakValueDictionary[str, asyncio.Lock] = WeakValueDictionary() + self._changelog: dict[str, Any] | None = None + self._changelog_lock = asyncio.Lock() + self._workspace = _WorkspaceBarrier() + + def _turn_lock(self, context_id: str) -> asyncio.Lock: + return self._turn_locks.setdefault(context_id, asyncio.Lock()) + + async def run(self, request: TaskRequest[LettaCodeConfig]) -> TaskResult: + # The CLI's JSON events, appended as it prints them, are the turn's + # trajectory: readable while it runs, and its final record once it ends. + request.trajectory.set_format(TRAJECTORY_FORMAT) + prompt = "\n".join(part.text for part in request.parts if isinstance(part, TextPart) and part.text) + context_id = request.context_id + async with self._workspace.shared(), self._turn_lock(context_id): + # Read under the lock rather than from the request: a snapshot load that held it may have just bound this + # context to an imported session. + session_id = self.session_ref_for_context(context_id) + + def record(event: dict) -> None: + request.trajectory.append(event) + # A first turn's session is bound as soon as the CLI names it, so a cancel can't lose it. + if self.session_ref_for_context(context_id) is None and (started := event_session_id(event)): + self.set_session_ref_for_context(context_id, started) + try: + attachments = [] + for part in request.parts: + if isinstance(part, FilePart): + attachments.extend(await asyncio.to_thread(_materialize_and_render, part, request.task_id)) + except FilePartMaterializeError as exc: + return TaskResult.failure("filepart_error", str(exc), error_type="infra_error") + try: + result = await run_letta_code( + prompt=prompt, + context_id=context_id, + session_id=session_id, + model=request.config.model, + effort=request.config.effort, + mcp_servers=dict(request.mcp_servers), + system_prompt=request.config.system_prompt, + output_format=request.config.output_format, + timeout_s=request.config.timeout_seconds, + project_id=request.config.project_id, + task_id=request.config.task_id, + role=request.config.role, + attachment_paths=attachments, + event_callback=record, + workspace=str(WORKSPACE_DIR), + ) + except Exception: + logger.exception("Letta Code invocation failed") + return TaskResult.failure( + "letta_code_invocation_error", "Letta Code invocation failed", error_type="infra_error" + ) + if result.session_id: + self.set_session_ref_for_context(context_id, result.session_id) + await self._capture_changelog() + + parts = [TextPart(text=result.response or result.error or "")] + usage = Usage(tool_call_count=result.tool_call_count) + if result.error: + details = {"error_type": "cli_error", "error_message": result.error[:ERROR_MESSAGE_LIMIT]} + if result.error_code: + details["error_code"] = result.error_code + parts.append(DataPart(data=details)) + return ( + TaskResult.builder() + .failed(result.error_code or "letta_code_cli_error", result.error[:ERROR_MESSAGE_LIMIT]) + .parts(parts) + .session_ref(result.session_id) + .usage(usage) + .build() + ) + response = ( + TaskResult.builder() + .succeeded() + .parts(parts) + .session_ref(result.session_id) + .usage(usage) + ) + if request.config.output_format is not None: + response.add_structured_output(getattr(result, "structured_output", None)) + return response.build() + + @extension(SKILL_CONFIG_V1.add.inline) + async def add_inline_skill(self, request: InlineSkillRequest) -> SkillAddResponse: + return await install_skill(request, SKILLS_DIR) + + @extension(SKILL_CONFIG_V1.add.bundle) + async def add_bundle_skill(self, request: BundleSkillRequest) -> SkillAddResponse: + return await install_skill(request, SKILLS_DIR) + + @extension(ATTRIBUTION_PROBE_V1.probe) + async def probe_attribution(self) -> AttributionProbeResponse: + """The LiteLLM attribution the last CLI run sent with its model calls.""" + snapshot = get_last_attribution_snapshot() + return AttributionProbeResponse( + last_seen_attribution={ + key: value for key, value in snapshot.items() if key != "last_seen_at_utc" and isinstance(value, str) + }, + last_seen_at_utc=snapshot.get("last_seen_at_utc"), + ) + + @extension(SNAPSHOT_V1.save) + async def save_snapshot(self, request: ObjectSnapshotSaveRequest) -> dict[str, Any]: + with tempfile.TemporaryDirectory(prefix="letta_code-snapshot-") as directory: + workspace = Path(directory) / "workspace.tgz" + async with self._workspace.exclusive(), self._turn_lock(request.context_id): + # Read once no turn runs: a context's first turn, or a load into it, may be about to bind it. + session_id = self.session_ref_for_context(request.context_id) + if not session_id: + raise HTTPException(status_code=404, detail="No active session for context_id") + session_json = await export_session(request.context_id, session_id) + if request.objects.workspace is not None: + await asyncio.to_thread( + subprocess.run, + ["tar", *_backup_exclusions(), "-czf", str(workspace), + "-C", str(WORKSPACE_DIR), "."], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + ) + objects = {"trajectory": (await upload(request.objects.trajectory, session_json)).model_dump(mode="json")} + if request.objects.workspace is not None: + objects["workspace"] = (await upload(request.objects.workspace, workspace)).model_dump(mode="json") + return {"context_id": request.context_id, "objects": objects} + + @extension(SNAPSHOT_V1.load) + async def load_snapshot(self, request: ObjectSnapshotLoadRequest) -> dict[str, Any]: + target_context_id = request.target_context_id or uuid.uuid4().hex + with tempfile.TemporaryDirectory(prefix="letta_code-restore-") as directory: + session_file = Path(directory) / "session.json" + await download(request.objects.trajectory, session_file) + staged = None + if request.objects.workspace is not None: + archive = Path(directory) / "workspace.tgz" + staged = Path(directory) / "workspace" + staged.mkdir() + await download(request.objects.workspace, archive) + await asyncio.to_thread( + subprocess.run, ["tar", "-xzf", str(archive), "-C", str(staged)], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + ) + async with self._workspace.exclusive(), self._turn_lock(target_context_id): + imported_session_id = await import_session(target_context_id, str(session_file)) + if not imported_session_id: + raise HTTPException(status_code=500, detail="Letta Code import returned no session id") + if staged is not None: + await asyncio.to_thread(_replace_workspace, staged) + self.set_session_ref_for_context(target_context_id, imported_session_id) + return {"context_id": target_context_id} + + async def _capture_changelog(self) -> None: + """One incremental tar of the changelog's roots, uploaded after each turn, with the roots removed since the + last; the baseline advances only once the upload succeeds, so a failed capture's changes, and its removals, go + with the next.""" + async with self._changelog_lock: + config = self._changelog + if config is None: + return + roots = [root for root in config["roots"] if Path(root).exists()] + removed = [root for root in config["present"] if root not in roots] + if not roots and not removed: + return + sequence = config["sequence"] + _CHANGELOG_DIR.mkdir(parents=True, exist_ok=True) + archive = _CHANGELOG_DIR / f"{sequence:06d}.tgz" + pending_snar = _CHANGELOG_DIR / "pending.snar" + record = _CHANGELOG_META_DIR / f"{sequence:06d}" + try: + await asyncio.to_thread(shutil.rmtree, record, True) + record.mkdir(parents=True) + (record / _META_NAME).write_text(json.dumps({"sequence": sequence, "removed": removed})) + await asyncio.to_thread(shutil.copyfile, _CHANGELOG_SNAR, pending_snar) + await asyncio.to_thread( + subprocess.run, + ["tar", f"--listed-incremental={pending_snar}", + *_changelog_backup_exclusions(), + f"--exclude={_CHANGELOG_DIR}", "-czf", str(archive), *roots, str(record)], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + ) + await config["uploader"].upload(f"{sequence:06d}.tgz", archive) + pending_snar.replace(_CHANGELOG_SNAR) + config["present"] = roots + config["sequence"] += 1 + except Exception: + logger.exception("Letta Code changelog capture failed") + finally: + archive.unlink(missing_ok=True) + pending_snar.unlink(missing_ok=True) + await asyncio.to_thread(shutil.rmtree, record, True) + + +class LettaCodeChangelogAgent(LettaCodeAgent): + """With snapshot/v1's changelog, which needs GNU tar's incremental archives. Letta Code has no hook after each tool + call, so an increment is a completed turn's changes; conversation state lives outside the captured roots, so + apply restores files only.""" + + @extension(SNAPSHOT_V1.changelog.enable) + async def enable_changelog(self, request: NamespaceChangelogEnableRequest) -> dict[str, Any]: + roots = request.roots or [str(WORKSPACE_DIR)] + async with self._workspace.exclusive(), self._changelog_lock: + await asyncio.to_thread(_baseline_changelog, roots) + self._changelog = {"roots": roots, "sequence": 0, "present": [root for root in roots if Path(root).exists()], + "uploader": NamespaceUploader(request.write_namespace)} + return {"roots": roots} + + @extension(SNAPSHOT_V1.changelog.apply) + async def apply_changelog(self, request: ObjectChangelogApplyRequest) -> dict[str, Any]: + async with self._workspace.exclusive(): + with tempfile.TemporaryDirectory(prefix="letta_code-changelog-apply-") as directory: + for increment in request.increments: + archive = Path(directory) / f"{increment.sequence:06d}.tgz" + await download(increment.object, archive) + await asyncio.to_thread(_apply_increment, archive, increment.sequence) + return {"count": len(request.increments), "context_id": request.target_context_id} + + +def _replace_workspace(staged: Path) -> None: + WORKSPACE_DIR.mkdir(parents=True, exist_ok=True) + backup = Path(tempfile.mkdtemp(prefix=".letta_code-backup-", dir=WORKSPACE_DIR)) + saved = backup / "saved" + saved.mkdir() + committed = False + try: + try: + for child in WORKSPACE_DIR.iterdir(): + if child != backup: + child.rename(saved / child.name) + except Exception: + for child in saved.iterdir(): + child.rename(WORKSPACE_DIR / child.name) + raise + try: + for child in staged.iterdir(): + shutil.move(str(child), str(WORKSPACE_DIR / child.name)) + committed = True + except Exception: + for child in WORKSPACE_DIR.iterdir(): + if child == backup: + continue + if child.is_dir() and not child.is_symlink(): + shutil.rmtree(child) + else: + child.unlink() + for child in saved.iterdir(): + child.rename(WORKSPACE_DIR / child.name) + raise + finally: + if committed: + try: + shutil.rmtree(backup) + except OSError: + logger.warning("Could not remove snapshot backup %s", backup, exc_info=True) + elif not any(saved.iterdir()): + saved.rmdir() + backup.rmdir() + + +def _apply_increment(archive: Path, sequence: int) -> None: + """Extract an increment over /, all but its record, then remove the roots its record names.""" + listing = subprocess.run(["tar", "--incremental", "-tzf", str(archive)], check=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE).stdout.decode(errors="replace").splitlines() + records = [member for member in listing if _META_MEMBER.search(member)] + # The record's directory is the capture's, not part of the layer it restores. + excluded = {member.rsplit("/", 1)[0] for member in records} + subprocess.run( + ["tar", "--incremental", "--no-wildcards", *(f"--exclude={member}" for member in sorted(excluded)), + "-xzf", str(archive), "-C", "/"], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + ) + for member in records: + record = subprocess.run(["tar", "-xzOf", str(archive), member], check=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE).stdout + try: + removed = json.loads(record).get("removed") + except (ValueError, AttributeError): + logger.warning(f"changelog increment {sequence:06d}: unreadable record {member}") + continue + for path in removed if isinstance(removed, list) else []: + if not _removable(path): + logger.warning(f"changelog increment {sequence:06d}: not removing {path!r}") + continue + if os.path.isdir(path) and not os.path.islink(path): + shutil.rmtree(path) + else: + os.unlink(path) + logger.info(f"changelog increment {sequence:06d}: removed root {path}") + + +def _removable(path: object) -> bool: + """Whether a removed root a record names may be removed on replay: an absolute, normalized path that exists and + isn't / or a top-level system directory.""" + return (isinstance(path, str) and os.path.isabs(path) and os.path.normpath(path) == path + and path not in _SYSTEM_DIRS and os.path.lexists(path)) + + +def _baseline_changelog(roots: list[str]) -> None: + _CHANGELOG_DIR.mkdir(parents=True, exist_ok=True) + _CHANGELOG_SNAR.unlink(missing_ok=True) + present = [root for root in roots if Path(root).exists()] + subprocess.run( + ["tar", f"--listed-incremental={_CHANGELOG_SNAR}", + *_changelog_backup_exclusions(), + "-cf", "/dev/null", *present], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + ) + + +def gnu_tar() -> bool: + """Whether this host's tar is GNU tar, which the changelog's incremental archives need.""" + try: + version = subprocess.run(["tar", "--version"], capture_output=True, text=True, + timeout=_TAR_VERSION_TIMEOUT_S).stdout + except (OSError, subprocess.SubprocessError): + return False + return "GNU tar" in version + + +def agent_for_this_host() -> LettaCodeAgent: + """The agent, with the changelog where this host's tar can make its archives.""" + return LettaCodeChangelogAgent() if gnu_tar() else LettaCodeAgent() + + +def build_app(agent: LettaCodeAgent) -> Starlette: + """The agent's Starlette app, answering errors with FastAPI's `{"detail": ...}` bodies.""" + return json_error_bodies(agent.create_app()) + + +app = build_app(agent_for_this_host()) +# agent-env's install/v1 deploys find the install parameters by importing this name from the image. +AGENT_CARD = app.state.agentenv_a2a.card diff --git a/src/agent_env/agents/agents/letta-code/entrypoint.sh b/src/agent_env/agents/agents/letta-code/entrypoint.sh new file mode 100644 index 000000000..c59e6c1c8 --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e +# A stop waits this long for open requests, then the server shuts down and kills the CLIs of turns still running. +exec uvicorn a2a_server:app --host 0.0.0.0 --port "${A2A_PORT:-8000}" --timeout-graceful-shutdown 5 diff --git a/src/agent_env/agents/agents/letta-code/install.sh b/src/agent_env/agents/agents/letta-code/install.sh new file mode 100644 index 000000000..1301e9906 --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/install.sh @@ -0,0 +1,175 @@ +#!/bin/bash +# Install this agent from the folder this script is in, and start it on PORT, without an image of its own: +# +# install.sh container NAME PORT into the running container NAME, as root, with /app as the CLI's workspace +# install.sh host NAME PORT WORKDIR onto this Linux host, as agent NAME, with WORKDIR as the CLI's workspace +# +# It brings Node and uv, each checked against its published checksum, the Letta Code CLI as package-lock.json pins it, and +# the agent's requirements in a private Python, so the target's own Node, Python and packages are never touched; a +# target with no Node or Python of its own gets these at the end of its PATH. Node's builds need glibc, and a target +# with no bash, which the model's commands run in, gets it from its own package manager. The model endpoint's +# LITELLM_API_KEY, LITELLM_BASE_URL and LITELLM_EXTRA_HEADERS, and AGENT_TRUST_ENV, the KEY='VALUE' lines of the CA +# variables the agent needs, come in the environment and are kept in a 0600 file the agent reads. +set -euo pipefail +umask 077 + +mode=$1 target=$2 port=$3 +here=$(cd "$(dirname "$0")" && pwd) + +UV_VERSION=0.13.1 +UV_SHA256_X86_64=6c86040a8f045ab9adc9e3ec4cdafa03fd00e9a988ed2bf28971c7fc2d0debd0 +UV_SHA256_AARCH64=345a8308e4d1835cdfaadf692836fd3137a6d45864773f05ad7bf59442b1ba16 +PYTHON_VERSION=3.12 +START_TIMEOUT_S=120 +STOP_TIMEOUT_S=10 +dockerfile_arg() { sed -n "s/^ARG $1=//p" "$here/Dockerfile"; } +NODE_VERSION=$(dockerfile_arg NODE_VERSION) +AGENT_FILES="a2a_server.py letta_code_bridge.py letta_code_kit.py requirements.txt run.sh" + +# put SRC DEST replaces DEST by a rename, so a binary an earlier install's agent still runs can be replaced; put_dir SRC +# DEST copies a folder to DEST, which isn't there. +case $mode in + container) + root=/opt/agentenv-agent-$port # one per agent: two in a container listen on two ports + workspace=/app + on() { docker exec -u 0 "$target" sh -c "$1"; } + put() { docker cp "$1" "$target:$2.new" > /dev/null && on "mv -f $(quoted "$2.new") $(quoted "$2")"; } + put_dir() { docker cp "$1" "$target:$2" > /dev/null; } + ;; + host) + [ "$(uname -s)" = Linux ] || { echo "a host install needs a Linux host, not $(uname -s)" >&2; exit 1; } + # Outside the work dir install_agent empties before each attempt, as a container's is, so a retry stops the agent an + # earlier attempt started. It ends in the sandbox's id, which names what a local sandbox's teardown removes from + # /tmp; a VM's host goes with the VM. + root=/tmp/agentenv-agent-$target-$port${AGENTENV_SANDBOX:+-$AGENTENV_SANDBOX} + workspace=$4 + on() { sh -c "$1"; } + put() { cp "$1" "$2.new" && mv -f "$2.new" "$2"; } + put_dir() { cp -R "$1" "$2"; } + ;; + *) echo "usage: install.sh container NAME PORT | install.sh host NAME PORT WORKDIR" >&2; exit 2 ;; +esac + +if [ "$mode" = host ]; then + # /tmp is shared, and the folder will hold the model key: it is this user's alone, and one another made is refused. + if ! mkdir -m 0700 "$root" 2> /dev/null; then + [ -d "$root" ] && [ ! -L "$root" ] && [ -O "$root" ] \ + || { echo "$root is already there and isn't a folder of this user's own" >&2; exit 1; } + chmod 0700 "$root" + fi +fi + +case $(on 'uname -m') in + x86_64|amd64) cpu=x64 uv_cpu=x86_64 uv_sha256=$UV_SHA256_X86_64 node_sha256=$(dockerfile_arg NODE_SHA256_X64) ;; + aarch64|arm64) cpu=arm64 uv_cpu=aarch64 uv_sha256=$UV_SHA256_AARCH64 node_sha256=$(dockerfile_arg NODE_SHA256_ARM64) ;; + *) echo "Node has no build for $(on 'uname -m')" >&2; exit 1 ;; +esac +if on 'for f in /lib/ld-musl-*; do [ -e "$f" ] && exit 0; done; exit 1'; then + echo "Node's builds need glibc, and the target's libc is musl" >&2 + exit 1 +fi + +if ! on 'command -v bash >/dev/null'; then + on 'if command -v apt-get >/dev/null; then apt-get update -qq && apt-get install -y -qq --no-install-recommends bash + elif command -v dnf >/dev/null; then dnf install -y -q bash + elif command -v microdnf >/dev/null; then microdnf install -y bash + else exit 1; fi' > /dev/null \ + || { echo "the model's commands run in bash: the target has none, and bash can't be added" >&2; exit 1; } +fi + +# Letta Code keeps agent memory in git, including on freshly installed targets. +if ! on 'command -v git >/dev/null'; then + on 'if command -v apt-get >/dev/null; then apt-get update -qq && apt-get install -y -qq --no-install-recommends git + elif command -v dnf >/dev/null; then dnf install -y -q git + elif command -v microdnf >/dev/null; then microdnf install -y git + else exit 1; fi' > /dev/null \ + || { echo "Letta Code requires git, and git cannot be added" >&2; exit 1; } +fi + +sha256() { if command -v sha256sum >/dev/null; then sha256sum "$1"; else shasum -a 256 "$1"; fi | cut -d' ' -f1; } +fetch() { curl -fsSL --retry 3 "$1" -o "$2"; } +checked() { # checked FILE SHA256: FILE is what was published, or the install stops + [ "$(sha256 "$1")" = "$2" ] || { echo "$(basename "$1") doesn't match its published checksum" >&2; exit 1; } +} +quoted() { printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/g")"; } +r=$(quoted "$root") # $root, as the commands run in the target read it + +# Beside this script, in the work dir the sandbox's teardown removes, so a killed install leaves nothing elsewhere. +stage=$(mktemp -d "$here/../stage.XXXXXX") +trap 'rm -rf "$stage"' EXIT + +node_archive=node-v$NODE_VERSION-linux-$cpu.tar.gz +fetch "https://nodejs.org/dist/v$NODE_VERSION/$node_archive" "$stage/$node_archive" +checked "$stage/$node_archive" "$node_sha256" +mkdir "$stage/node" +tar -xzf "$stage/$node_archive" -C "$stage/node" --strip-components=1 + +uv_archive=uv-$uv_cpu-unknown-linux-musl.tar.gz +fetch "https://github.com/astral-sh/uv/releases/download/$UV_VERSION/$uv_archive" "$stage/$uv_archive" +checked "$stage/$uv_archive" "$uv_sha256" +tar -xzf "$stage/$uv_archive" -C "$stage" +chmod 0755 "$stage/uv-$uv_cpu-unknown-linux-musl/uv" + +# Use the private Node to execute the pinned CLI. +printf '#!/bin/sh\nexec %s %s "$@"\n' "$(quoted "$root/node/bin/node")" \ + "$(quoted "$root/npm/node_modules/@letta-ai/letta-code/letta.js")" > "$stage/letta" +chmod 0755 "$stage/letta" + +# The model endpoint and CA, as the agent's environment, readable by its user only. +{ + for name in LITELLM_API_KEY LITELLM_BASE_URL LITELLM_EXTRA_HEADERS; do + printf '%s=%s\n' "$name" "$(quoted "${!name:-}")" + done + [ -z "${AGENT_TRUST_ENV:-}" ] || printf '%s\n' "$AGENT_TRUST_ENV" + # Letta Code's state, its sessions and skills, as the image keeps it under /data. + printf 'A2A_PORT=%s\nLETTA_CODE_WORKSPACE_DIR=%s\nHOME=%s\n' \ + "$(quoted "$port")" "$(quoted "$workspace")" "$(quoted "$root/data")" + for dir in CONFIG:config CACHE:cache DATA:share STATE:state; do + printf 'XDG_%s_HOME=%s\n' "${dir%%:*}" "$(quoted "$root/data/${dir#*:}")" + done +} > "$stage/agent.env" + +# An earlier install's agent, a retried step's say, stops first: its files are replaced and its port is freed. +on "if [ -f $r/agent.pid ]; then pid=\$(cat $r/agent.pid); kill \$pid 2> /dev/null; i=0; \ + while [ \$i -lt $STOP_TIMEOUT_S ] && kill -0 \$pid 2> /dev/null; do sleep 1; i=\$((i + 1)); done; \ + kill -9 \$pid 2> /dev/null; rm -f $r/agent.pid; fi; true" + +# The agent starts afresh, with no sessions or skills of one it replaces. +on "rm -rf $r/data $r/node $r/npm && mkdir -p $r/bin $r/app $r/data $r/npm $(quoted "$workspace")" +put_dir "$stage/node" "$root/node" +put "$stage/uv-$uv_cpu-unknown-linux-musl/uv" "$root/bin/uv" +put "$stage/letta" "$root/bin/letta" +put "$stage/agent.env" "$root/agent.env" +put "$here/package.json" "$root/npm/package.json" +put "$here/package-lock.json" "$root/npm/package-lock.json" +for file in $AGENT_FILES; do + put "$here/$file" "$root/app/$file" +done +on "chmod 0600 $r/agent.env && chmod 0755 $r/bin/uv $r/bin/letta $r/app/run.sh" + +# Install only headless dependencies' required scripts, then the private Python environment. +on "cd $r/npm && export PATH=$r/node/bin:\$PATH npm_config_cache=$r/cache/npm npm_config_update_notifier=false \ + && npm ci --ignore-scripts --omit=dev --no-audit --no-fund --loglevel=error > /dev/null \ + && npm rebuild @letta-ai/letta-code sharp protobufjs > /dev/null \ + && cd $r && export UV_PYTHON_PREFERENCE=only-managed UV_PYTHON_INSTALL_DIR=$r/python \ + UV_CACHE_DIR=$r/cache/uv && bin/uv venv --quiet --clear --python $PYTHON_VERSION venv \ + && bin/uv pip install --quiet --python venv/bin/python -r app/requirements.txt \ + && rm -rf $r/cache" + +if [ "$mode" = container ]; then + docker exec -d -u 0 "$target" sh -c "exec $r/app/run.sh $r > $r/agent.log 2>&1" +else + cd "$workspace" + nohup "$root/app/run.sh" "$root" > "$root/agent.log" 2>&1 < /dev/null & +fi + +# Done once the agent answers on its port; an agent that exits first fails the install with the end of its log. +health="import urllib.request; urllib.request.urlopen('http://127.0.0.1:$port/health', timeout=2)" +for _ in $(seq "$START_TIMEOUT_S"); do + on "$r/venv/bin/python -c \"$health\"" 2> /dev/null && exit 0 + on "[ ! -f $r/agent.pid ] || kill -0 \$(cat $r/agent.pid)" 2> /dev/null || break + sleep 1 +done +echo "the agent didn't start; the end of its log:" >&2 +on "tail -n 40 $r/agent.log" >&2 || true +exit 1 diff --git a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py new file mode 100644 index 000000000..500c30360 --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py @@ -0,0 +1,498 @@ +"""Letta Code local-backend bridge, with isolated context state and a per-turn model relay.""" +from __future__ import annotations + +import asyncio +import contextlib +import hashlib +import json +import logging +import os +import io +import shutil +import tarfile +import tempfile +import signal +import subprocess +import threading +from contextlib import asynccontextmanager + +from aiohttp import ClientSession, ClientTimeout, web + +from collections.abc import Callable, Mapping +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path + +from jsonschema import Draft202012Validator, SchemaError, ValidationError + +logger = logging.getLogger(__name__) + +# The model a run uses when neither the task nor the agent-config names one; the +# @ai-sdk/openai-compatible provider forwards it verbatim as the chat model. +DEFAULT_MODEL = os.environ.get("LETTA_CODE_MODEL", "openai/gpt-4.1-mini") +ERROR_MESSAGE_LIMIT = 4000 +ROLE_HEADER = "AgentEnv-Role" +# What agent-env gives an agent for [model.headers]: a JSON object of header names to values. +MODEL_HEADERS_ENV = "LITELLM_EXTRA_HEADERS" + +# Root under which each A2A context_id gets its own OpenCode data dir; lives +# under $HOME so host-mode instances don't share session state. +SESSIONS_ROOT = os.environ.get("LETTA_CODE_SESSIONS_ROOT") or os.path.join(os.environ.get("HOME", "/data"), "sessions") +_CLI_TIMEOUT_S = 120 + + +def _base_url(environ: Mapping[str, str]) -> str: + """The endpoint's chat-completions root: agent-env's LITELLM_BASE_URL has no /v1 suffix.""" + base = (environ.get("LITELLM_BASE_URL") or "").rstrip("/") + return base if base.endswith("/v1") else f"{base}/v1" + + +def context_data_dir(context_id: str) -> Path: + """Per-context OpenCode data dir. A stable short hash keeps the path clean + and filesystem-safe regardless of the context_id's charset.""" + ctx_hash = hashlib.sha1(context_id.encode()).hexdigest()[:16] + return Path(SESSIONS_ROOT) / ctx_hash + + +def _context_env(context_id: str) -> dict[str, str]: + home = context_data_dir(context_id) + home.mkdir(parents=True, exist_ok=True) + return {**os.environ, "HOME": str(home), "LETTA_LOCAL_BACKEND_DIR": str(home / ".letta/lc-local-backend"), + "LETTA_LOCAL_BACKEND_EXPERIMENTAL": "1", "LETTA_CODE_DISABLE_AUTOUPDATE": "1"} + + +async def _communicate(*cmd: str, env: Mapping[str, str], timeout_s: int) -> tuple[int, bytes, bytes]: + """Run a short CLI command to its end; one that outlives ``timeout_s`` is killed.""" + proc = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=dict(env), + ) + try: + out, err = await asyncio.wait_for(proc.communicate(), timeout=timeout_s) + except BaseException: + with contextlib.suppress(ProcessLookupError): + proc.kill() + await proc.wait() + raise + return proc.returncode, out, err + + + +async def export_session(context_id: str, session_id: str) -> bytes: + """Capture the conversation, agent, git-backed memory and local settings together.""" + root = context_data_dir(context_id) + (root / "snapshot-session.json").write_text(json.dumps({"session_id": session_id})) + buffer = io.BytesIO() + with tarfile.open(fileobj=buffer, mode="w:gz") as archive: + archive.add(root, arcname="state") + return buffer.getvalue() + + +async def import_session(context_id: str, json_path: str) -> str: + """Stage a complete local state tree, then replace the target context's state.""" + root = context_data_dir(context_id) + root.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(dir=root.parent) as directory: + with tarfile.open(json_path, "r:gz") as archive: + archive.extractall(directory, filter="data") + staged = Path(directory) / "state" + session_id = json.loads((staged / "snapshot-session.json").read_text())["session_id"] + if not isinstance(session_id, str) or not session_id: + raise ValueError("Snapshot has no conversation id") + # Memory repos and settings may contain absolute paths to the original context. + source = json.loads((staged / "context.json").read_text())["home"] + for path in staged.rglob("*.json"): + if ".git" not in path.parts: + path.write_text(path.read_text().replace(source, str(root))) + if root.exists(): + shutil.rmtree(root) + shutil.move(str(staged), root) + return session_id + + +@dataclass +class LettaCodeResult: + response: str + tool_call_count: int + session_id: str | None + trajectory: list[dict] = field(default_factory=list) + error: str | None = None + error_code: str | None = None + structured_output: object | None = None + + +def validate_output_format(output_format: dict) -> dict: + if output_format.get("type") != "json_schema" or not isinstance(output_format.get("schema"), dict): + raise ValueError("output_format must contain type=json_schema and a schema object") + try: + Draft202012Validator.check_schema(output_format["schema"]) + except SchemaError as exc: + raise ValueError("output_format contains an invalid JSON schema") from exc + return output_format["schema"] + + +# The attribution the last run sent, which /ext/attribution-probe reads back so the +# validator can confirm project_id/task_id were wired through. +_last_attribution: dict[str, str | None] = { + "project_id": None, + "task_id": None, + "last_seen_at_utc": None, +} +_attribution_lock = threading.Lock() + + +def _record_attribution(project_id: str | None, task_id: str | None) -> None: + if not project_id and not task_id: + return + with _attribution_lock: + _last_attribution["project_id"] = project_id or None + _last_attribution["task_id"] = task_id or None + _last_attribution["last_seen_at_utc"] = datetime.now(timezone.utc).isoformat() + + +def get_last_attribution_snapshot() -> dict[str, str | None]: + with _attribution_lock: + return dict(_last_attribution) + + +def _attribution_headers(project_id: str | None, task_id: str | None) -> dict[str, str]: + """LiteLLM cost-attribution headers, x-litellm-customer-id + x-litellm-tags; {} when nothing to attribute.""" + headers: dict[str, str] = {} + if project_id: + headers["x-litellm-customer-id"] = project_id + tags = [f"projectId:{project_id}"] + if task_id: + tags.append(f"taskId:{task_id}") + headers["x-litellm-tags"] = ",".join(tags) + elif task_id: + headers["x-litellm-tags"] = f"taskId:{task_id}" + return headers + + +def model_headers(raw: str | None) -> dict[str, str]: + if not raw: + return {} + try: + headers = json.loads(raw) + except json.JSONDecodeError: + logger.warning(f"ignoring {MODEL_HEADERS_ENV}, which isn't JSON") + return {} + if not isinstance(headers, dict): + logger.warning(f"ignoring {MODEL_HEADERS_ENV}, which isn't a JSON object") + return {} + return {str(name): str(value) for name, value in headers.items()} + + +def provider_headers(environ: Mapping[str, str], attribution: Mapping[str, str]) -> dict[str, str]: + """The endpoint's [model.headers], then the run's attribution, each replacing a header of the same name in any + case.""" + return {name.lower(): value for layer in (model_headers(environ.get(MODEL_HEADERS_ENV)), attribution) + for name, value in layer.items()} + + +def _mcp_headers(headers: Mapping[str, str] | None, role: str | None) -> dict[str, str]: + """A server's registered headers; the run's role, when it has one, replaces a role header they name.""" + merged = dict(headers or {}) + if role: + merged = {name: value for name, value in merged.items() if name.lower() != ROLE_HEADER.lower()} + merged[ROLE_HEADER] = role + return merged + + +_PROCESS_CLEANUP_TIMEOUT_S = 5 +_RUNNING: set = set() + + +def kill_running_clis() -> int: + """Kill every CLI still running a turn, and what each started; how many. The agent's shutdown does: the CLIs, in + sessions of their own, would outlive its server, as when a reinstall stops it mid-turn.""" + running = [proc for proc in _RUNNING if proc.returncode is None] + for proc in running: + _kill_cli(proc) + return len(running) + + +def _kill_cli(proc) -> None: + """Kill the CLI and everything under it: its process group, and, found by parent, the tools a CLI starts in a + session of their own, which no group or session kill reaches.""" + pid = getattr(proc, "pid", None) + if pid is None: + with contextlib.suppress(ProcessLookupError): + proc.kill() + return + if proc.returncode is not None: + return # the CLI is gone, and its pid may be another process's by now + tree = [pid, *_descendants(pid)] + with contextlib.suppress(ProcessLookupError, PermissionError): + os.killpg(pid, signal.SIGKILL) + for member in tree: + with contextlib.suppress(ProcessLookupError, PermissionError): + os.kill(member, signal.SIGKILL) + + +async def _started(spawn): + """The CLI ``spawn`` starts. A turn cancelled while it starts lets it finish starting, then kills it and what it + started: asyncio would kill the CLI alone, and leave its tools running.""" + start = asyncio.ensure_future(spawn) + try: + return await asyncio.shield(start) + except asyncio.CancelledError: + with contextlib.suppress(Exception): + proc = await start + _kill_cli(proc) + await _reap(proc) + raise + + +async def _reap(proc) -> None: + """Wait for the killed CLI, but not for a process that left its tree and still holds its pipes open.""" + with contextlib.suppress(asyncio.TimeoutError): + await asyncio.wait_for(proc.wait(), _PROCESS_CLEANUP_TIMEOUT_S) + + +def _descendants(pid: int) -> list[int]: + """The processes under ``pid``, by parent, read from /proc; none where there is no /proc.""" + children: dict[int, list[int]] = {} + for entry in os.listdir("/proc") if os.path.isdir("/proc") else (): + if not entry.isdigit(): + continue + try: + with open(f"/proc/{entry}/stat") as stat: + parent = int(stat.read().rsplit(")", 1)[1].split()[1]) + except (OSError, ValueError, IndexError): + continue + children.setdefault(parent, []).append(int(entry)) + found, pending = [], [pid] + while pending: + for child in children.get(pending.pop(), ()): + found.append(child) + pending.append(child) + return found + + + +@asynccontextmanager +async def model_relay(headers: dict[str, str]): + """Keep endpoint credentials and task attribution on requests to the configured model endpoint only.""" + base = _base_url(os.environ) + async with ClientSession(timeout=ClientTimeout(total=None), auto_decompress=False) as client: + async def forward(request): + outgoing = {"authorization": "Bearer " + os.environ.get("LITELLM_API_KEY", ""), **headers} + if request.content_type: + outgoing["content-type"] = request.headers.get("Content-Type", "application/json") + async with client.request(request.method, base + request.path_qs.removeprefix("/v1"), + data=await request.read(), headers=outgoing) as upstream: + response = web.StreamResponse(status=upstream.status, headers={ + name: value for name, value in upstream.headers.items() + if name.lower() not in {"content-length", "transfer-encoding", "connection"} + }) + await response.prepare(request) + async for chunk in upstream.content.iter_any(): + await response.write(chunk) + await response.write_eof() + return response + app = web.Application(client_max_size=100 * 1024**2) + app.router.add_route("*", "/{path:.*}", forward) + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, "127.0.0.1", 0) + await site.start() + port = site._server.sockets[0].getsockname()[1] + try: + yield f"http://127.0.0.1:{port}/v1" + finally: + await runner.cleanup() + + +async def _command(env: dict[str, str], *args: str) -> bytes: + code, out, err = await _communicate("letta", "--backend", "local", *args, env=env, timeout_s=120) + if code: + raise RuntimeError(f"Letta Code {args[0]} failed: {err.decode(errors='replace')[-ERROR_MESSAGE_LIMIT:]}") + return out + + +def _configure_agent(home: Path, agent_id: str, servers: dict, role: str | None, system_prompt: str | None): + # Core memory's system/ files are compiled into the existing agent's system prompt on every run. + memory = home / ".letta/lc-local-backend/memfs" / agent_id / "memory" + memory.mkdir(parents=True, exist_ok=True) + instructions = system_prompt or "" + if servers: + instructions += ("\nMCP tools are available through the Letta CLI. Before calling any MCP tool, run " + "`letta mcp tools --agent " + agent_id + "` to discover exact names and schemas. " + "Use the full mcp____ name returned by discovery, never a bare tool name. " + "Call `letta mcp call --agent " + agent_id + " --args ''`. " + "Use these tools to act on connected services and peer agents.\n") + target = "task_instructions.md" if (memory / "MEMORY.md").exists() else "system/task_instructions.md" + path = memory / target + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("---\nname: Task instructions\ndescription: Instructions for the current task\n---\n" + + (instructions or "Follow the user's task.\n")) + # The local backend projects committed core memory, not uncommitted working files. + for args in (["add", "--", target], ["-c", "user.name=Letta Code", "-c", "user.email=code@letta.com", + "-c", "commit.gpgsign=false", "commit", "--quiet", "--allow-empty", + "-m", "Update task instructions", "--", target]): + subprocess.run(["git", *args], cwd=memory, check=True, capture_output=True) + + settings_path = home / ".letta/settings.json" + settings = json.loads(settings_path.read_text()) if settings_path.exists() else {} + agents = settings.setdefault("agents", []) + matching = [agent for agent in agents if agent.get("agentId") == agent_id] + if not matching: + matching = [{"agentId": agent_id, "baseUrl": "local:" + str(home / ".letta/lc-local-backend")}] + agents.extend(matching) + for agent in matching: + agent["mcpServers"] = [ + {"name": name, "transport": "http", "url": info["url"], + "headers": _mcp_headers(info.get("headers"), role)} for name, info in servers.items() + ] + settings_path.parent.mkdir(parents=True, exist_ok=True) + settings_path.write_text(json.dumps(settings)) + + +async def run_letta_code( + prompt: str, context_id: str, session_id: str | None = None, model: str | None = None, + mcp_servers: dict[str, dict] | None = None, system_prompt: str | None = None, + output_format: dict | None = None, timeout_s: int = 600, project_id: str | None = None, + task_id: str | None = None, role: str | None = None, attachment_paths: list[str] | None = None, + effort: str | None = None, event_callback: Callable[[dict], None] | None = None, workspace: str = "/app", +) -> LettaCodeResult: + """A bounded CLI turn, including provider setup; cancellation kills the process tree.""" + try: + async with asyncio.timeout(timeout_s): + return await _turn(prompt, context_id, session_id, model, mcp_servers, system_prompt, output_format, + project_id, task_id, role, attachment_paths, effort, event_callback, workspace) + except TimeoutError: + return LettaCodeResult(response="", tool_call_count=0, session_id=session_id, + error=f"Letta Code timed out after {timeout_s}s") + + +async def _turn(prompt, context_id, session_id, model, mcp_servers, system_prompt, output_format, + project_id, task_id, role, attachment_paths, effort, event_callback, workspace): + model = model or DEFAULT_MODEL + handle = "openai-compatible/" + model + env = _context_env(context_id) + home = Path(env["HOME"]) + (home / "context.json").write_text(json.dumps({"home": str(home)})) + attribution = _attribution_headers(project_id, task_id) + async with model_relay(provider_headers(os.environ, attribution)) as relay: + await _command(env, "connect", "openai-compatible", "--base-url", relay, "--api-key", "relay") + agent_file = home / "agent-id.json" + if agent_file.exists(): + agent_id = json.loads(agent_file.read_text())["id"] + else: + agent = json.loads(await _command(env, "agents", "create", "--model", handle)) + agent_id = agent["id"] + agent_file.write_text(json.dumps({"id": agent_id})) + _configure_agent(home, agent_id, mcp_servers or {}, role, system_prompt) + skills = Path(os.environ.get("HOME", "/data")) / ".letta/skills" + if skills.exists(): + shutil.copytree(skills, home / ".letta/skills", dirs_exist_ok=True) + # The shell tools inherit these, so nested `letta mcp` calls select the same backend and agent. + env["LETTA_AGENT_ID"] = agent_id + if output_format is not None: + schema = validate_output_format(output_format) + prompt += "\nReturn only JSON matching this schema: " + json.dumps(schema) + for path in attachment_paths or []: + prompt += "\nRead the attached file at " + path + cmd = ["letta", "--backend", "local", "-p", prompt, "--output-format", "stream-json", "--yolo", + "--model", handle, "--agent", agent_id, "--toolset", "default"] + cmd += ["--conversation", session_id] if session_id else ["--new"] + if effort: + cmd += ["--reasoning", effort] + proc = await _started(asyncio.create_subprocess_exec( + *cmd, cwd=workspace, env=env, stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, start_new_session=True, + )) + _RUNNING.add(proc) + parser = _EventParser(event_callback) + _record_attribution(project_id, task_id) + try: + stderr = await _stream_cli(proc, parser.on_line) + except BaseException: + _kill_cli(proc) + await _reap(proc) + raise + finally: + _RUNNING.discard(proc) + result = parser.result(stderr, proc.returncode) + if output_format is not None and result.error is None: + try: + result.structured_output = json.loads(result.response) + Draft202012Validator(schema).validate(result.structured_output) + except (ValueError, ValidationError) as exc: + result.error = f"Letta Code response does not match output_format: {exc}" + result.error_code = "invalid_output_format" + return result + + +# How much of the CLI's stdout one read takes; a line may span several reads. +_STDOUT_READ_BYTES = 64 * 1024 + + +async def _stream_cli(proc, on_line: Callable[[str], None]) -> bytes: + """``proc.communicate()``, handing ``on_line`` each stdout line as it arrives. + + Lines are split as ``stdout.decode(errors="replace").splitlines()`` splits + the whole output, so they are the same lines; a line has no length limit, + as with ``communicate``. Returns stderr once the CLI has exited.""" + stderr_task = asyncio.create_task(proc.stderr.read()) + try: + # A line's pieces, joined once when its newline arrives: re-joining them on every read + # would cost a long line the square of its length. + pieces: list[bytes] = [] + while chunk := await proc.stdout.read(_STDOUT_READ_BYTES): + *ends, rest = chunk.split(b"\n") + for end in ends: + pieces.append(end) + for line in b"".join(pieces).decode(errors="replace").splitlines(): + on_line(line) + pieces = [] + pieces.append(rest) + for line in b"".join(pieces).decode(errors="replace").splitlines(): + on_line(line) + stderr = await stderr_task + await proc.wait() + return stderr + finally: + stderr_task.cancel() + + + +def event_session_id(event: dict) -> str | None: + return event.get("conversation_id") + + +class _EventParser: + def __init__(self, event_callback=None): + self.callback = event_callback + self.trajectory = [] + self.session_id = None + self.response = "" + self.tools = set() + self.completed = False + self.error = None + + def on_line(self, line: str): + try: + event = json.loads(line) + except ValueError: + return + if not isinstance(event, dict): + return + self.trajectory.append(event) + self.session_id = event_session_id(event) or self.session_id + if event.get("message_type") == "tool_call_message": + self.tools.add((event.get("tool_call") or {}).get("tool_call_id") or event.get("uuid") or str(len(self.tools))) + if event.get("type") == "result": + self.completed = event.get("subtype") == "success" + self.response = event.get("result") or "" + if not self.completed: + self.error = event.get("stop_reason") or self.response or "Letta Code did not complete" + if self.callback: + self.callback(event) + + def result(self, stderr: bytes, returncode: int | None) -> LettaCodeResult: + error = None if self.completed else (self.error or f"Letta Code exited {returncode} without completing") + if error: + error = (error + "\n" + stderr.decode(errors="replace")[-ERROR_MESSAGE_LIMIT:])[-ERROR_MESSAGE_LIMIT:] + return LettaCodeResult(response=self.response, tool_call_count=len(self.tools), session_id=self.session_id, + trajectory=self.trajectory, error=error) diff --git a/src/agent_env/agents/agents/letta-code/letta_code_kit.py b/src/agent_env/agents/agents/letta-code/letta_code_kit.py new file mode 100644 index 000000000..d38eb0634 --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/letta_code_kit.py @@ -0,0 +1,75 @@ +"""The pieces of agent plumbing this agent shares in shape with other CLI agents, copied from the Claude Code agent's +``kit.py``: durable session references, skill installs and FastAPI-style error bodies.""" + +from __future__ import annotations + +import logging +import shutil +from pathlib import Path + +from agentenv_protocol.a2a_agent import BundleSkillRequest, InlineSkillRequest, SkillAddResponse +from agentenv_protocol.transfers import download +from starlette.applications import Starlette +from starlette.exceptions import HTTPException +from starlette.requests import Request +from starlette.responses import JSONResponse, Response + +logger = logging.getLogger(__name__) + +_REFS_ATTR = "_durable_session_refs" + + +class DurableSessionRefs: + """Mixin, listed before ``AgentEnvAgent``, that keeps every context's session reference for the life of the + process: the framework's own record is a bounded LRU, so an idle context would otherwise start a fresh CLI + session.""" + + def session_ref_for_context(self, context_id: str) -> str | None: + return self.__dict__.get(_REFS_ATTR, {}).get(context_id) or super().session_ref_for_context(context_id) + + def set_session_ref_for_context(self, context_id: str, session_ref: str) -> None: + super().set_session_ref_for_context(context_id, session_ref) + self.__dict__.setdefault(_REFS_ATTR, {})[context_id] = session_ref + + +_NO_BODY_STATUSES = frozenset({204, 205, 304}) + + +async def _detail_body(_request: Request, exc: HTTPException) -> Response: + if exc.status_code < 200 or exc.status_code in _NO_BODY_STATUSES: + return Response(status_code=exc.status_code, headers=exc.headers) + return JSONResponse({"detail": exc.detail}, status_code=exc.status_code, headers=exc.headers) + + +def json_error_bodies(app: Starlette) -> Starlette: + """Answer every ``HTTPException`` with a ``{"detail": ...}`` body, as agent-env reads errors.""" + app.add_exception_handler(HTTPException, _detail_body) + return app + + +def _validate_skill_name(name: str) -> None: + if name != Path(name).name or ".." in name or "/" in name or "\\" in name: + raise HTTPException(status_code=400, detail=f"Invalid skill name '{name}': must not contain path separators or '..'") + + +async def install_skill(request: InlineSkillRequest | BundleSkillRequest, skills_dir: Path) -> SkillAddResponse: + """Write ``skills_dir//``: the inline ``SKILL.md``, or every bundle file through its read grant. A failed + install leaves no directory behind.""" + _validate_skill_name(request.name) + skill_dir = skills_dir / request.name + if skill_dir.exists(): + raise HTTPException(status_code=409, detail=f"Skill '{request.name}' is already registered") + skill_dir.mkdir(parents=True) + try: + if isinstance(request, BundleSkillRequest): + for item in request.skill_bundle.files: + await download(item.object, skill_dir / item.path) + else: + if not request.skill_md or not request.skill_md.strip(): + raise HTTPException(status_code=400, detail="Skill content must not be empty") + (skill_dir / "SKILL.md").write_text(request.skill_md) + except BaseException: + shutil.rmtree(skill_dir, ignore_errors=True) + raise + logger.info(f"Registered skill '{request.name}' at {skill_dir}") + return SkillAddResponse(name=request.name) diff --git a/src/agent_env/agents/agents/letta-code/package-lock.json b/src/agent_env/agents/agents/letta-code/package-lock.json new file mode 100644 index 000000000..36635e93f --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/package-lock.json @@ -0,0 +1,5447 @@ +{ + "name": "letta-code", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@letta-ai/letta-code": "0.34.25" + } + }, + "node_modules/@letta-ai/letta-code": { + "version": "0.34.25", + "resolved": "https://registry.npmjs.org/@letta-ai/letta-code/-/letta-code-0.34.25.tgz", + "integrity": "sha512-OrtcoobIWUikWOR6cJGQgZ7uma9fN4kjt8M+OxEtoMBIkaKV4EL7nQ2DWV6Br1rs+PH7vUpGcn/Xl0PKrDAyVw==", + "hasInstallScript": true, + "hasShrinkwrap": true, + "license": "Apache-2.0", + "dependencies": { + "@earendil-works/pi-ai": "^0.99.1", + "@janhapke/sharp-electron": "0.35.3-electron.1", + "@letta-ai/letta-agent-sdk": "0.8.38", + "@letta-ai/letta-client": "^1.10.2", + "@letta-ai/trajectory": "0.2.0", + "@modelcontextprotocol/sdk": "1.30.0", + "@pierre/diffs": "1.2.2", + "@scarf/scarf": "^1.4.0", + "@shikijs/langs": "4.0.2", + "cron-parser": "^5.6.1", + "cross-spawn": "^7.0.6", + "glob": "^13.0.0", + "ink": "5.2.1", + "ink-link": "^4.1.0", + "node-pty": "^1.1.0", + "open": "^10.2.0", + "react": "18.2.0", + "sharp": "^0.34.5", + "shiki": "^4.0.2", + "strip-ansi": "^7.2.0", + "ws": "^8.19.0" + }, + "bin": { + "letta": "letta.js" + }, + "engines": { + "bun": ">=1.3.2", + "node": "^22.23.4 || ^24.22.1 || ^26.12.1" + }, + "optionalDependencies": { + "@vscode/ripgrep": "^1.17.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@alcalzone/ansi-tokenize": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", + "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@anthropic-ai/sdk": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.124.0.tgz", + "integrity": "sha512-cN5O8i9UVxHeOQAzj/XjshWXG8KiibJDw9OGpH2Z/eR3n/RBxdoLxDJOcfqAJWvjaMDFfHTBADU04hWRJVkDyA==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1127.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1127.0.tgz", + "integrity": "sha512-IDl/lrPb90aH+pZFHGNDmgH9nAUQj5PlZH1sJ3w7RikctyjHSnY3oNjZhrLoaBoQn/rNK0zsP6OHEqEhj2tdLA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/credential-provider-node": "^3.972.82", + "@aws-sdk/eventstream-handler-node": "^3.972.34", + "@aws-sdk/middleware-eventstream": "^3.972.29", + "@aws-sdk/middleware-websocket": "^3.972.52", + "@aws-sdk/token-providers": "3.1127.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/core": { + "version": "3.978.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.5.tgz", + "integrity": "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.975.0", + "@aws-sdk/xml-builder": "^3.972.41", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.36.1", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.76.tgz", + "integrity": "sha512-YxmL1Ar7bV9ZkJvtv+lj+MF6le76NnZnbmgySyZuxpZaCv0/1lZ3KxKy6RIj1h9ui2hWnVjZxbmTMeO0y/NrQA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.78", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.78.tgz", + "integrity": "sha512-3VHknDRngoebsaqyT1iR75b5cBT2zfEtODK6yYmYltljkK1HEAQLs2FEd1IpWqAeOYdMGaG9wikngWLTufY7RA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.21.tgz", + "integrity": "sha512-aLx9hbCX6mGAyv4ZZd8UagJ4RASHVJw6BFDSIyuKmLkqNL6H47EjBPK6qaJ4B5TDV6mc+vMa0SJFYyo4mkvVQg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/credential-provider-env": "^3.972.76", + "@aws-sdk/credential-provider-http": "^3.972.78", + "@aws-sdk/credential-provider-login": "^3.972.83", + "@aws-sdk/credential-provider-process": "^3.972.76", + "@aws-sdk/credential-provider-sso": "^3.973.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.82", + "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.83", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.83.tgz", + "integrity": "sha512-9bJU6FjRacF4FK/QHf8Dyxhur7B1grRj8C1Yh4IkpoFW2exQflPdCv0VrbemLW9ZctLNle+AE7TETs9KNvuLYg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.88", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.88.tgz", + "integrity": "sha512-AG0KID5qhQPzrFkh5b0Cws1Amytvzo0L0Xj6hMgJXEwD6ZXAqninDhSNAWJiN5zU4YVVZnuidnqwPwYJ04IamA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.76", + "@aws-sdk/credential-provider-http": "^3.972.78", + "@aws-sdk/credential-provider-ini": "^3.973.21", + "@aws-sdk/credential-provider-process": "^3.972.76", + "@aws-sdk/credential-provider-sso": "^3.973.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.82", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.76.tgz", + "integrity": "sha512-pdhKFTXv1zPYhh92MTlp0jVbLWvUEV9v+rziuEkoskGGQ2iJskxHTO38nA8UAbT/N4IM3n8OZJ9rIY5IBO15/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.20.tgz", + "integrity": "sha512-DlxrAB3MnjNLx/da7b89W+3DyJQYso/q7Ns15MsST2IV2nkAYQOYTUCaffNSjgrpMP99Dm2mV2jxzcbyF94ZfA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/token-providers": "3.1155.0", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1155.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1155.0.tgz", + "integrity": "sha512-iIcQnYLhWuq9qOLQbIWFrxF3PF0svVeZvHnUS4DUnO4zUFmFHIyGyjqrntXG+e/5PUgfavV0pYfDIKHxTzN2LA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.82", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.82.tgz", + "integrity": "sha512-LUo46IqPTJ4xKBsrLuoaxiv/D2GTBCcFDMD1y3jKIqPR/p7Rw14RJFZ0iRWftqZtyLPQX9JMGnSi3mBu4y/pNA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.38.tgz", + "integrity": "sha512-K7vPKNstDlavHFZgbmcFdJOFIyowsevjddycRziRBbtXH/D9z+i9juCnH17liwSX9T8fJiDQzlEYkRtxkR+1vA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.33", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.33.tgz", + "integrity": "sha512-8QUt39D/dk5tdV7rLvIfMMI/ilQff4zth7mkMKpVOLM+M5AQ9W3nPxFVuHQANevmVWi4T5MlxqfKV4Q2drYeBA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.58", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.58.tgz", + "integrity": "sha512-/BvocdloKozx/zFr4benH+9J1ywB/2dYFHPWNUdo0CBCe00gTm0HPflOq8MH6cuCTbnywSrscGq0/LheN59teQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.50", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.50.tgz", + "integrity": "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.5", + "@aws-sdk/signature-v4-multi-region": "^3.996.49", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.36.1", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.49.tgz", + "integrity": "sha512-nvuAZNRrRRVCiARRKsYlQ0VnXiEQiq7vXPH+UA+LSo+kN3Djl9eo/V2r0udkw85PnsNshmv2Em1clld5M3j67w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.975.0", + "@smithy/signature-v4": "^5.7.4", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/token-providers": { + "version": "3.1127.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1127.0.tgz", + "integrity": "sha512-Dv2TMWBshJ+tF6ahs2Sy5bh4Iabsd4GAQqVvE9XZmYmnoaVbpS2QKIKE/HRacc7bTtbjEEvP+laGzHvHlf1CiQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.9", + "@aws-sdk/nested-clients": "^3.997.44", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/types": { + "version": "3.975.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.975.0.tgz", + "integrity": "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz", + "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@babel/runtime": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@earendil-works/pi-ai": { + "version": "0.99.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.99.2.tgz", + "integrity": "sha512-9RFOEdY+ZTJ1AI+UuAFs4RM0tF2Tje/R/CEv9gWTJHt0iTu8XHZs64U/EIZxNSllFmec/4HfwsOxYj1qQek9bg==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.124.0", + "@aws-sdk/client-bedrock-runtime": "3.1127.0", + "@earendil-works/pi-telemetry": "^0.99.2", + "@google/genai": "2.21.0", + "@smithy/node-http-handler": "4.12.1", + "http-proxy-agent": "9.1.0", + "https-proxy-agent": "9.1.0", + "openai": "7.19.0", + "partial-json": "0.1.7", + "typebox": "1.3.27" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@earendil-works/pi-telemetry": { + "version": "0.99.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.99.2.tgz", + "integrity": "sha512-WThYU4XM6jjjzH4FzLdreN7QAPS9SDdokL0W0Ldheg1ssCIJkfpK7PgebFm7/PoQub7OiXx+SRF6B/oF7z7lXA==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@google/genai": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.21.0.tgz", + "integrity": "sha512-+PDtco2/Z0ONdzCGekCoCT+O1VJS9xJQNN4XzQpXG/t3El/SWWMkCWlFRO1KmivOHPa4Q0VjUYu1HBKCZ/v33Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@hono/node-server": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.5.tgz", + "integrity": "sha512-MgP/I06GdvsilDspMk/qmRzpk8LOUStP6jFAbSRi40W09DhoyUn+Mj7eyKICPqeYKybOjVFLVIAQzvu1bfIa7Q==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4 || ^5.0.0-0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-freebsd-wasm32/node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-webcontainers-wasm32/node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron": { + "version": "0.35.3-electron.1", + "resolved": "https://registry.npmjs.org/@janhapke/sharp-electron/-/sharp-electron-0.35.3-electron.1.tgz", + "integrity": "sha512-ZDnFPMsKolANxS+RDjgxw6/jc6O0JfJ0msu6bgBdPCz6NFm4+K+cPurwXB+rdl5UY2/EQ5bzQ7ENYnRXtdprLQ==", + "license": "Apache-2.0", + "dependencies": { + "sharp": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@janhapke/sharp-electron/node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-agent-sdk": { + "version": "0.8.38", + "resolved": "https://registry.npmjs.org/@letta-ai/letta-agent-sdk/-/letta-agent-sdk-0.8.38.tgz", + "integrity": "sha512-ayMN6fNMRlj368RPRPDBVYQQDujxcawqEnsNmyefEgIm0j0q1+33VZpAUNfVAvB0Vivvx4FTQDx/E9Lbmpafhw==", + "license": "Apache-2.0", + "dependencies": { + "@letta-ai/letta-client": "^1.12.1", + "ajv": "^8.20.0" + }, + "engines": { + "node": ">=22.19.0" + }, + "peerDependencies": { + "@letta-ai/letta-code": ">=0.34.9 <0.35.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-client": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@letta-ai/letta-client/-/letta-client-1.12.1.tgz", + "integrity": "sha512-rYjXMXpkfssj7VBBX3qCp6mdpNRv6YPNrliYsjkhWoQDqGg3J9bsgIQ28ZhQTddabYxRUIwcdzuaizFx8pvZ7A==", + "license": "Apache-2.0" + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/@pierre/diffs": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@pierre/diffs/-/diffs-1.2.2.tgz", + "integrity": "sha512-MvWLv2oSOJOF8oYXWLdhicguHM11G/VNWu6OPR5ZETolp2NM2/KPQG3cZTnKpJ6ImqEHwvw6Gl6z2gmmy2FQmQ==", + "extraneous": true, + "license": "apache-2.0", + "dependencies": { + "@pierre/theme": "1.0.3", + "@shikijs/transformers": "^3.0.0", + "diff": "8.0.3", + "hast-util-to-html": "9.0.5", + "lru_map": "0.4.1", + "shiki": "^3.0.0" + }, + "peerDependencies": { + "react": "^18.3.1 || ^19.0.0", + "react-dom": "^18.3.1 || ^19.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/@pierre/diffs/node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/@pierre/diffs/node_modules/shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", + "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/engine-javascript": "3.23.0", + "@shikijs/engine-oniguruma": "3.23.0", + "@shikijs/langs": "3.23.0", + "@shikijs/themes": "3.23.0", + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/@shikijs/core": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/@shikijs/engine-javascript": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/trajectory": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@letta-ai/trajectory/-/trajectory-0.2.0.tgz", + "integrity": "sha512-biCyT0z8nh4Q8kIqBrPgmzoalacPmosmCpvEjdN9ILpL85+T75b8ahcN9MHPHQUVRj/J7UyQuRahJ4/JdrpCcA==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@pierre/diffs": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@pierre/diffs/-/diffs-1.2.2.tgz", + "integrity": "sha512-MvWLv2oSOJOF8oYXWLdhicguHM11G/VNWu6OPR5ZETolp2NM2/KPQG3cZTnKpJ6ImqEHwvw6Gl6z2gmmy2FQmQ==", + "license": "apache-2.0", + "dependencies": { + "@pierre/theme": "1.0.3", + "@shikijs/transformers": "^3.0.0", + "diff": "8.0.3", + "hast-util-to-html": "9.0.5", + "lru_map": "0.4.1", + "shiki": "^3.0.0" + }, + "peerDependencies": { + "react": "^18.3.1 || ^19.0.0", + "react-dom": "^18.3.1 || ^19.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@pierre/theme": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@pierre/theme/-/theme-1.0.3.tgz", + "integrity": "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA==", + "license": "MIT", + "engines": { + "vscode": "^1.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@letta-ai/letta-code/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@letta-ai/letta-code/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@letta-ai/letta-code/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@letta-ai/letta-code/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@letta-ai/letta-code/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@letta-ai/letta-code/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@letta-ai/letta-code/node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@letta-ai/letta-code/node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.5.0.tgz", + "integrity": "sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.5.0", + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/core/node_modules/@shikijs/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", + "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/engine-javascript": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.5.0.tgz", + "integrity": "sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/engine-javascript/node_modules/@shikijs/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", + "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/engine-oniguruma": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.5.0.tgz", + "integrity": "sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/engine-oniguruma/node_modules/@shikijs/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", + "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/langs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", + "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/primitive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.5.0.tgz", + "integrity": "sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/primitive/node_modules/@shikijs/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", + "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/themes": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.5.0.tgz", + "integrity": "sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/themes/node_modules/@shikijs/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", + "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/transformers": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.23.0.tgz", + "integrity": "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/transformers/node_modules/@shikijs/core": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/transformers/node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", + "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@shikijs/vscode-textmate": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.1.0.tgz", + "integrity": "sha512-cVBuhDQjBZaNMTwBquYfPydrUP+E3hE6oYGasx8h6ZrODIM7/r+gxLDXdd66oszbrggZHEntdE6j+5gYIVyMWQ==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/@smithy/core": { + "version": "3.36.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.2.tgz", + "integrity": "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@smithy/signature-v4": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz", + "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@smithy/types": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.18.0.tgz", + "integrity": "sha512-ns5lWe44tSfbTMbVUsyB+I1819PVSw4AdpgK0RNkzfWfwy6+3IUNSxwSrfTno1/oWaS/hERNz+XLWVyga2aJBQ==", + "license": "MIT", + "optional": true, + "optionalDependencies": { + "@vscode/ripgrep-darwin-arm64": "1.18.0", + "@vscode/ripgrep-darwin-x64": "1.18.0", + "@vscode/ripgrep-linux-arm": "1.18.0", + "@vscode/ripgrep-linux-arm64": "1.18.0", + "@vscode/ripgrep-linux-ia32": "1.18.0", + "@vscode/ripgrep-linux-ppc64": "1.18.0", + "@vscode/ripgrep-linux-riscv64": "1.18.0", + "@vscode/ripgrep-linux-s390x": "1.18.0", + "@vscode/ripgrep-linux-x64": "1.18.0", + "@vscode/ripgrep-win32-arm64": "1.18.0", + "@vscode/ripgrep-win32-ia32": "1.18.0", + "@vscode/ripgrep-win32-x64": "1.18.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep-darwin-arm64": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep-darwin-arm64/-/ripgrep-darwin-arm64-1.18.0.tgz", + "integrity": "sha512-r3ktHSvbFycQNF6sl7sNDPocpsI7J+mEzh1IaZFkY0spm3k2Z9t8hPAeOK7+p0l6p6/swkQC14XWX01low+94Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep-darwin-x64": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep-darwin-x64/-/ripgrep-darwin-x64-1.18.0.tgz", + "integrity": "sha512-25b4gWbL138dGuQU244ebCKKc0q05ULBMoFSz9oAEUHNeqK/lOJViDS7DRvbDazzAzSEdan391Znks/R5mkaTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep-linux-arm": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-arm/-/ripgrep-linux-arm-1.18.0.tgz", + "integrity": "sha512-GDAvufNDHu8zqLEmXstalQF0Wh6wQvdsBi/Vg3Yi3CK4a8XoFXqqXVEHEZ9xQz3t0NfoSEc9JbvK9DDS6FxyxQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep-linux-arm64": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-arm64/-/ripgrep-linux-arm64-1.18.0.tgz", + "integrity": "sha512-lQ/5zTG++U0E3IhVgS4EPTTn/U4okncaRMM5GOFfOYZywS4nuD31GhkHbNYlDk5CuDC68+hYJ0/eQeyCKJDA+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep-linux-ia32": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-ia32/-/ripgrep-linux-ia32-1.18.0.tgz", + "integrity": "sha512-YWLkSUtFd4Jh5EepIhA9RJSfv3uMAVMo+2rBIGHPBnvgLrZciIs2cDKei1/p6Wc/aCzUoHyMAg2R6tw4ZCBKGg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep-linux-ppc64": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-ppc64/-/ripgrep-linux-ppc64-1.18.0.tgz", + "integrity": "sha512-quXVY8fwQ8O/lvU1yrSqSl3jlUzysRSb+AfUfCL/tRtphxsKlFvPAejryZ6vg4Bgvn8XL74xb4qMCDmWgYrT5w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep-linux-riscv64": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-riscv64/-/ripgrep-linux-riscv64-1.18.0.tgz", + "integrity": "sha512-f5kBQBrWfQt8Q7OhSORuNDei5dkYagBj3y4jImSUXGMy8B/Ke7SltSRcUtjPv166FAFfHCAmWuZp3+cWnX2/Vw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep-linux-s390x": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-s390x/-/ripgrep-linux-s390x-1.18.0.tgz", + "integrity": "sha512-rTOcJFGGcl2c07RUOWUo4U1ndnemKhY6A9hnMB18uk7jSgJc0d/QLBGWMWpumdtoJtpizn/wIv5mXIisJukusQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep-linux-x64": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep-linux-x64/-/ripgrep-linux-x64-1.18.0.tgz", + "integrity": "sha512-mQ3bVrUpnD2vs7QT0vX90Lt0cnUq467uFtEktIdsJJmW296RoSULRGqWgzG1AKxyBpNDD6l4ZO4qKf6SgyC23Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep-win32-arm64": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep-win32-arm64/-/ripgrep-win32-arm64-1.18.0.tgz", + "integrity": "sha512-vfTIjq1OHnzUjxZcHVQAMbnggp8dpGf+0QKFOZHwWPqFwXxQC8eCWM+5NUdoJ6yrElCeMzoUTXoK/LdZaniB+Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep-win32-ia32": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep-win32-ia32/-/ripgrep-win32-ia32-1.18.0.tgz", + "integrity": "sha512-//rfAE+BOw5AC2EMmepmiE36jUuevtQYNQqqlw1s3m9FlRxjxEut97RkRPHAu9BG4mSojatZx+kXZXNdyI9caQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/@vscode/ripgrep-win32-x64": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep-win32-x64/-/ripgrep-win32-x64-1.18.0.tgz", + "integrity": "sha512-KNPvtElldqILHdnAetujPaowkNbpqJy3ssIGGN6F6Kve9Qi+nNLI2DN01O83JjCEVQbCzl8Ov3QZ9Eov3BR8Dg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/body-parser": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz", + "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.1.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.3", + "on-finished": "^2.4.1", + "qs": "^6.16.1", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/@letta-ai/letta-code/node_modules/bundle-name": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/content-disposition": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz", + "integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/cron-parser": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.10.2.tgz", + "integrity": "sha512-JfAYMY0LZaWQTuict8PhtF/ksSnq8xefSDmQUnnoqvkPYkU/y3UP0DTWzJKFBz0P1IXsDG8q0vY3zHkoFZOhvw==", + "license": "MIT", + "dependencies": { + "luxon": "^3.7.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@letta-ai/letta-code/node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/es-toolkit": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.53.0.tgz", + "integrity": "sha512-3+AYXp9fmJ/UhHBMYj2/U+RaUouMD90SzoItyDOB2Za7W6dB7fQApFGnZPl2MF6K8/Tv1koh1JyirSx/kju6Pw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, + "node_modules/@letta-ai/letta-code/node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/express": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.3.0.tgz", + "integrity": "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.3.0", + "content-disposition": "^2.0.1", + "content-type": "^2.0.0", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.8", + "qs": "^6.16.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.1.0", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/express-rate-limit": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.1.tgz", + "integrity": "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/express/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/@letta-ai/letta-code/node_modules/fast-uri": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/@letta-ai/letta-code/node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/google-auth-library": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz", + "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/hono": { + "version": "4.13.14", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.14.tgz", + "integrity": "sha512-yhRLQ21U6qa9Hsu2N7Qcbh8tE+WKEcoErjIo56LeSGiGXGp9sA7UpE86Ui9uexb61KakiGwuHMn2QdV7w6i6kQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/http-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", + "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/https-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", + "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/@letta-ai/letta-code/node_modules/ink": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", + "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.22.0", + "indent-string": "^5.0.0", + "is-in-ci": "^1.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^7.2.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ink-link": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ink-link/-/ink-link-4.1.0.tgz", + "integrity": "sha512-3nNyJXum0FJIKAXBK8qat2jEOM41nJ1J60NRivwgK9Xh92R5UMN/k4vbz0A9xFzhJVrlf4BQEmmxMgXkCE1Jeg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1", + "terminal-link": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + }, + "peerDependencies": { + "ink": ">=4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ip-address": { + "version": "10.7.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz", + "integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/@letta-ai/letta-code/node_modules/jose": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz", + "integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/@letta-ai/letta-code/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/@letta-ai/letta-code/node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/lru_map": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.4.1.tgz", + "integrity": "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/micromark-util-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/node-pty": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz", + "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/on-finished": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.2.tgz", + "integrity": "sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/openai": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-7.19.0.tgz", + "integrity": "sha512-MX2s3u2L5racTO0CC/SWpCOasJQBCJrqLKXK+l82cAhdeF8mPMBEe/gxMm0ZFa2xpKpOFLRjxv5afYEZbBXmbQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": ">=3.972.0 <4", + "@smithy/hash-node": ">=4.3.0 <5", + "@smithy/signature-v4": ">=5.4.0 <6", + "undici": ">=5 <9", + "ws": "^8.21.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@smithy/hash-node": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "undici": { + "optional": true + }, + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@letta-ai/letta-code/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/proxy-agent-negotiate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", + "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "kerberos": "^2.0.0" + }, + "peerDependenciesMeta": { + "kerberos": { + "optional": true + } + } + }, + "node_modules/@letta-ai/letta-code/node_modules/qs": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", + "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/@letta-ai/letta-code/node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/shiki": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.5.0.tgz", + "integrity": "sha512-OcRW72ngQvc+MSVWBRIovC/BTW589iUE/Vxav7VeM4RcY9MUjqB+IR6ILG1k9nuW6LiG/nct9uma4/MBtnZbmQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.5.0", + "@shikijs/engine-javascript": "4.5.0", + "@shikijs/engine-oniguruma": "4.5.0", + "@shikijs/langs": "4.5.0", + "@shikijs/themes": "4.5.0", + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/shiki/node_modules/@shikijs/langs": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.5.0.tgz", + "integrity": "sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/shiki/node_modules/@shikijs/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", + "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/@letta-ai/letta-code/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/standardwebhooks": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.1.1.tgz", + "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/terminal-link": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-3.0.0.tgz", + "integrity": "sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^5.0.0", + "supports-hyperlinks": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/terminal-link/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/terminal-link/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@letta-ai/letta-code/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/typebox": { + "version": "1.3.27", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.27.tgz", + "integrity": "sha512-zu+jc1pcy4UiNThxikUr36f0Rybk9PEeCg/NE6adeWr/SKsdNO4EzZHYRDlv2YCVAfj3Odq3dESSo/jNyoBXzA==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/unist-util-visit": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.2.0.tgz", + "integrity": "sha512-fpsYs/VVoZxfGY9/j3FQpZmN8wdLJaV1Pt38AaJNZu9uKIHcFU3halCdBveEq1HGKO1OEfJY65w1zvoFV1b3qQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/unist-util-visit-parents": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.1.0.tgz", + "integrity": "sha512-vKXxz6O38sKDj1mtx/k2cWvkkFBuYGx6uGrlzBkVeWCRF/F8sdI0j3Wbkz/5rVNRRuVc8UrgRdbCxNhmYXgP3w==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/@letta-ai/letta-code/node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@letta-ai/letta-code/node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, + "node_modules/@letta-ai/letta-code/node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/src/agent_env/agents/agents/letta-code/package.json b/src/agent_env/agents/agents/letta-code/package.json new file mode 100644 index 000000000..e5c707973 --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "dependencies": { + "@letta-ai/letta-code": "0.34.25" + } +} diff --git a/src/agent_env/agents/agents/letta-code/requirements.txt b/src/agent_env/agents/agents/letta-code/requirements.txt new file mode 100644 index 000000000..bed7e8952 --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/requirements.txt @@ -0,0 +1,6 @@ +# The image installs the newest agentenv-framework-protocol at or above this floor; a release doesn't pin it, so the +# image is built again only when this folder changes. The floor is the first that serves peer-agents/v1's tools, which +# the agent advertises without handlers of its own; raise it when the agent needs a newer protocol. +agentenv-framework-protocol[agent]>=0.1.351 +jsonschema>=4.18,<5 +aiohttp>=3.9,<4 diff --git a/src/agent_env/agents/agents/letta-code/run.sh b/src/agent_env/agents/agents/letta-code/run.sh new file mode 100644 index 000000000..a40bd7c28 --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/run.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# Start an installed agent from ROOT, in this shell's working directory: its environment from ROOT/agent.env, its own +# Python and the Letta Code CLI, and its Node and Python for the model's commands on a target that has none. +set -e +root=$1 +set -a +. "$root/agent.env" +set +a +echo $$ > "$root/agent.pid" +export PATH="$root/bin:$PATH:$root/node/bin:$root/venv/bin" PYTHONPATH="$root/app" +# Within install.sh's stop timeout, so a reinstall's stop reaches the shutdown that kills running turns' CLIs. +exec "$root/venv/bin/uvicorn" a2a_server:app --host 0.0.0.0 --port "$A2A_PORT" --timeout-graceful-shutdown 5 diff --git a/src/agent_env/agents/aliases.toml b/src/agent_env/agents/aliases.toml index 02d350ed3..03725f230 100644 --- a/src/agent_env/agents/aliases.toml +++ b/src/agent_env/agents/aliases.toml @@ -6,3 +6,4 @@ gemini = ["gemini"] opencode = ["opencode"] pi = ["pi"] openhands = ["openhands"] +letta-code = ["letta-code"] diff --git a/src/agent_env/agents/tasks/letta-code-smoke.json b/src/agent_env/agents/tasks/letta-code-smoke.json new file mode 100644 index 000000000..7efb0ace3 --- /dev/null +++ b/src/agent_env/agents/tasks/letta-code-smoke.json @@ -0,0 +1,7 @@ +[ + {"id": "deploy", "type": "deploy_agent", "a2a_agent_id": "letta-code", "env_ids": [], "sandbox_type": "local"}, + {"id": "ask", "type": "prompt_agent", "prompt_id": "ask", "depends_on": ["deploy"], + "prompt": "Run the shell command `echo $((6 * 7))` and reply with only the number it prints."}, + {"id": "check", "type": "agent_prompt_response_verifier", "prompt_id": "ask", "verifier_id": "answer", + "criteria": [{"type": "response_contains", "needles": ["42"]}], "depends_on": ["ask"]} +] diff --git a/tst/unit/agents/letta_code/agent.py b/tst/unit/agents/letta_code/agent.py new file mode 100644 index 000000000..b29d366ab --- /dev/null +++ b/tst/unit/agents/letta_code/agent.py @@ -0,0 +1,20 @@ +"""The Letta Code agent's modules, imported from its folder as its image runs them: by their own names, from the folder +on the path.""" + +import importlib +import importlib.util +import sys +from pathlib import Path + +import agent_env + +AGENT_DIR = Path(agent_env.__file__).parent / "agents" / "agents" / "letta-code" +sys.path.insert(0, str(AGENT_DIR)) +letta_code_bridge = importlib.import_module("letta_code_bridge") +letta_code_kit = importlib.import_module("letta_code_kit") +# By its path: another agent's a2a_server, imported by the same name, may be loaded already. +_spec = importlib.util.spec_from_file_location("letta_code_a2a_server", AGENT_DIR / "a2a_server.py") +a2a_server = importlib.util.module_from_spec(_spec) +# Registered first: pydantic resolves its config's annotations through the module's entry. +sys.modules[_spec.name] = a2a_server +_spec.loader.exec_module(a2a_server) From ee87d4ebe99d7398d3c80198476f79a68c13c424 Mon Sep 17 00:00:00 2001 From: Letta Integration <300689746+letta-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:31:30 +0000 Subject: [PATCH 02/13] fix(agents): commit task instructions and resume Letta conversations --- .../agents/agents/letta-code/README.md | 2 +- .../agents/letta-code/letta_code_bridge.py | 47 +++---- tst/unit/agents/letta_code/test_bridge.py | 116 ++++++++++++++++++ 3 files changed, 141 insertions(+), 24 deletions(-) create mode 100644 tst/unit/agents/letta_code/test_bridge.py diff --git a/src/agent_env/agents/agents/letta-code/README.md b/src/agent_env/agents/agents/letta-code/README.md index e295c5de3..4fb6fe9ec 100644 --- a/src/agent_env/agents/agents/letta-code/README.md +++ b/src/agent_env/agents/agents/letta-code/README.md @@ -7,7 +7,7 @@ is the whole build context. The Dockerfile pins the Python base by digest and No Each turn runs `letta --backend local -p --output-format stream-json --yolo`. The bridge creates an agent with `letta agents create`; the first turn uses `--agent --new`, and subsequent turns use -`--agent --conversation `. Each context has a separate home under `$HOME/sessions/`. Different contexts +`--conversation `. Each context has a separate home under `$HOME/sessions/`. Different contexts run concurrently; turns in one context and snapshot operations are synchronized. The bridge connects the local backend with `letta connect openai-compatible`. A per-turn loopback relay forwards diff --git a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py index 500c30360..4f3bf2d77 100644 --- a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py +++ b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py @@ -4,40 +4,38 @@ import asyncio import contextlib import hashlib +import io import json import logging import os -import io import shutil -import tarfile -import tempfile import signal import subprocess +import tarfile +import tempfile import threading -from contextlib import asynccontextmanager - -from aiohttp import ClientSession, ClientTimeout, web - from collections.abc import Callable, Mapping +from contextlib import asynccontextmanager from dataclasses import dataclass, field from datetime import datetime, timezone from pathlib import Path +from aiohttp import ClientSession, ClientTimeout, web from jsonschema import Draft202012Validator, SchemaError, ValidationError logger = logging.getLogger(__name__) # The model a run uses when neither the task nor the agent-config names one; the -# @ai-sdk/openai-compatible provider forwards it verbatim as the chat model. -DEFAULT_MODEL = os.environ.get("LETTA_CODE_MODEL", "openai/gpt-4.1-mini") +# local backend forwards it verbatim as the chat model. +DEFAULT_MODEL = "openai/gpt-4.1-mini" ERROR_MESSAGE_LIMIT = 4000 ROLE_HEADER = "AgentEnv-Role" # What agent-env gives an agent for [model.headers]: a JSON object of header names to values. MODEL_HEADERS_ENV = "LITELLM_EXTRA_HEADERS" -# Root under which each A2A context_id gets its own OpenCode data dir; lives +# Root under which each A2A context_id gets its own Letta Code data dir; lives # under $HOME so host-mode instances don't share session state. -SESSIONS_ROOT = os.environ.get("LETTA_CODE_SESSIONS_ROOT") or os.path.join(os.environ.get("HOME", "/data"), "sessions") +SESSIONS_ROOT = os.path.join(os.environ.get("HOME", "/data"), "sessions") _CLI_TIMEOUT_S = 120 @@ -48,7 +46,7 @@ def _base_url(environ: Mapping[str, str]) -> str: def context_data_dir(context_id: str) -> Path: - """Per-context OpenCode data dir. A stable short hash keeps the path clean + """Per-context Letta Code data dir. A stable short hash keeps the path clean and filesystem-safe regardless of the context_id's charset.""" ctx_hash = hashlib.sha1(context_id.encode()).hexdigest()[:16] return Path(SESSIONS_ROOT) / ctx_hash @@ -58,21 +56,23 @@ def _context_env(context_id: str) -> dict[str, str]: home = context_data_dir(context_id) home.mkdir(parents=True, exist_ok=True) return {**os.environ, "HOME": str(home), "LETTA_LOCAL_BACKEND_DIR": str(home / ".letta/lc-local-backend"), - "LETTA_LOCAL_BACKEND_EXPERIMENTAL": "1", "LETTA_CODE_DISABLE_AUTOUPDATE": "1"} + "LETTA_LOCAL_BACKEND_EXPERIMENTAL": "1"} async def _communicate(*cmd: str, env: Mapping[str, str], timeout_s: int) -> tuple[int, bytes, bytes]: """Run a short CLI command to its end; one that outlives ``timeout_s`` is killed.""" - proc = await asyncio.create_subprocess_exec( - *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=dict(env), - ) + proc = await _started(asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=dict(env), start_new_session=True, + )) + _RUNNING.add(proc) try: out, err = await asyncio.wait_for(proc.communicate(), timeout=timeout_s) except BaseException: - with contextlib.suppress(ProcessLookupError): - proc.kill() - await proc.wait() + _kill_cli(proc) + await _reap(proc) raise + finally: + _RUNNING.discard(proc) return proc.returncode, out, err @@ -317,7 +317,7 @@ def _configure_agent(home: Path, agent_id: str, servers: dict, role: str | None, instructions = system_prompt or "" if servers: instructions += ("\nMCP tools are available through the Letta CLI. Before calling any MCP tool, run " - "`letta mcp tools --agent " + agent_id + "` to discover exact names and schemas. " + "`letta mcp tools --full --agent " + agent_id + "` to discover exact names and schemas. " "Use the full mcp____ name returned by discovery, never a bare tool name. " "Call `letta mcp call --agent " + agent_id + " --args ''`. " "Use these tools to act on connected services and peer agents.\n") @@ -394,10 +394,11 @@ async def _turn(prompt, context_id, session_id, model, mcp_servers, system_promp for path in attachment_paths or []: prompt += "\nRead the attached file at " + path cmd = ["letta", "--backend", "local", "-p", prompt, "--output-format", "stream-json", "--yolo", - "--model", handle, "--agent", agent_id, "--toolset", "default"] - cmd += ["--conversation", session_id] if session_id else ["--new"] + "--model", handle, "--toolset", "default"] + cmd += ["--conversation", session_id] if session_id else ["--agent", agent_id, "--new"] if effort: - cmd += ["--reasoning", effort] + target = ["--conversation", session_id] if session_id else ["--agent", agent_id] + await _command(env, "model", "set", handle, "--reasoning", effort, *target) proc = await _started(asyncio.create_subprocess_exec( *cmd, cwd=workspace, env=env, stdin=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, start_new_session=True, diff --git a/tst/unit/agents/letta_code/test_bridge.py b/tst/unit/agents/letta_code/test_bridge.py new file mode 100644 index 000000000..bb4c561fa --- /dev/null +++ b/tst/unit/agents/letta_code/test_bridge.py @@ -0,0 +1,116 @@ +"""Exercise state restore, system instructions, event parsing and cancellation without model calls.""" +import asyncio +import json +import subprocess +from contextlib import asynccontextmanager + +import pytest + +from tst.unit.agents.letta_code.agent import a2a_server, letta_code_bridge as bridge + + +@pytest.mark.asyncio +async def test_snapshot_restores_memory_and_conversation_in_another_context(monkeypatch, tmp_path): + monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path / "sessions")) + source = bridge.context_data_dir("source") + source.mkdir(parents=True) + (source / "context.json").write_text(json.dumps({"home": str(source)})) + (source / "agent-id.json").write_text('{"id":"agent-local-1"}') + memory = source / ".letta/lc-local-backend/memfs/agent-local-1/memory" + memory.mkdir(parents=True) + (memory / "MEMORY.md").write_text("Remember violet otters") + conversation = source / "conversation.json" + conversation.write_text(json.dumps({"id": "local-conv-7", "memory": str(memory), "messages": ["secret"]})) + snapshot = tmp_path / "state.tgz" + snapshot.write_bytes(await bridge.export_session("source", "local-conv-7")) + target = bridge.context_data_dir("target") + target.mkdir() + (target / "stale").write_text("old target") + assert await bridge.import_session("target", str(snapshot)) == "local-conv-7" + assert not (target / "stale").exists() + assert (target / memory.relative_to(source) / "MEMORY.md").read_text() == "Remember violet otters" + restored = json.loads((target / "conversation.json").read_text()) + assert restored["messages"] == ["secret"] + assert restored["memory"] == str(target / memory.relative_to(source)) + assert (source / "conversation.json").exists() + + +def test_system_prompt_is_committed_and_servers_keep_role_headers(tmp_path): + memory = tmp_path / ".letta/lc-local-backend/memfs/agent-local-1/memory" + memory.mkdir(parents=True) + subprocess.run(["git", "init", "-q", str(memory)], check=True) + (memory / "MEMORY.md").write_text("index") + bridge._configure_agent(tmp_path, "agent-local-1", { + "mail": {"url": "https://mail.example/mcp", "headers": {"Authorization": "token", "AgentEnv-Role": "old"}}, + }, "reader", "End every answer with ZEBRA") + committed = subprocess.check_output(["git", "show", "HEAD:task_instructions.md"], cwd=memory).decode() + assert "End every answer with ZEBRA" in committed + assert "mcp____" in committed + settings = json.loads((tmp_path / ".letta/settings.json").read_text()) + server = settings["agents"][0]["mcpServers"][0] + assert server["headers"] == {"Authorization": "token", "AgentEnv-Role": "reader"} + bridge._configure_agent(tmp_path, "agent-local-1", {}, None, "Use TIGER now") + committed = subprocess.check_output(["git", "show", "HEAD:task_instructions.md"], cwd=memory).decode() + assert "TIGER" in committed and "ZEBRA" not in committed + + +def test_native_events_track_conversation_tools_and_stderr(): + events = [] + parser = bridge._EventParser(events.append) + for event in [ + {"type": "system", "conversation_id": "local-conv-2"}, + {"type": "message", "message_type": "tool_call_message", "tool_call": {"tool_call_id": "call-1"}}, + {"type": "message", "message_type": "tool_call_message", "tool_call": {"tool_call_id": "call-1"}}, + {"type": "result", "subtype": "success", "result": "42"}, + ]: + parser.on_line(json.dumps(event)) + result = parser.result(b"", 0) + assert result.response == "42" and result.session_id == "local-conv-2" + assert result.tool_call_count == 1 and result.error is None + assert len(events) == 4 + assert "provider failed" in bridge._EventParser().result(b"provider failed", 1).error + + +@pytest.mark.asyncio +async def test_cancel_kills_the_turn_process(monkeypatch, tmp_path): + monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path / "sessions")) + agent = a2a_server.LettaCodeAgent() + app = a2a_server.build_app(agent) + assert app.state.agentenv_a2a.card.name == "Letta Code" + started = asyncio.Event() + stopped = asyncio.Event() + + async def command(*args, **kwargs): + return b'{"id":"agent-local-1"}' + + async def stream(proc, callback): + started.set() + await asyncio.Event().wait() + + class Proc: + returncode = None + def kill(self): + self.returncode = -9 + stopped.set() + async def wait(self): + return self.returncode + + @asynccontextmanager + async def relay(*args): + yield "http://relay/v1" + + monkeypatch.setattr(bridge, "model_relay", relay) + + async def spawn(*args, **kwargs): + return Proc() + + monkeypatch.setattr(bridge, "_command", command) + monkeypatch.setattr(bridge, "_configure_agent", lambda *args: None) + monkeypatch.setattr(bridge.asyncio, "create_subprocess_exec", spawn) + monkeypatch.setattr(bridge, "_stream_cli", stream) + turn = asyncio.create_task(bridge.run_letta_code("wait", "ctx", workspace=str(tmp_path))) + await asyncio.wait_for(started.wait(), 5) + turn.cancel() + with pytest.raises(asyncio.CancelledError): + await turn + assert stopped.is_set() and not bridge._RUNNING From b3f77bc3ce2ae88a91eb0cbfb6c9c2f00e43a6ba Mon Sep 17 00:00:00 2001 From: Letta Integration <300689746+letta-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:31:30 +0000 Subject: [PATCH 03/13] test(agents): include Letta Code in shipped alias contract --- tst/unit/bundle/shipped_bundles_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tst/unit/bundle/shipped_bundles_test.py b/tst/unit/bundle/shipped_bundles_test.py index 44495505c..e42496f27 100644 --- a/tst/unit/bundle/shipped_bundles_test.py +++ b/tst/unit/bundle/shipped_bundles_test.py @@ -45,7 +45,8 @@ def test_the_shipped_aliases_are_agent_envs_alone_and_claude_code_names_the_defa assert {alias: named.target_id for alias, named in table.aliases.items()} == { "claude-code": "@local/agentenv-framework/agents/claude-code", "pi": "@local/agentenv-framework/agents/pi", "openhands": "@local/agentenv-framework/agents/openhands", "codex": "@local/agentenv-framework/agents/codex", - "gemini": "@local/agentenv-framework/agents/gemini", "opencode": "@local/agentenv-framework/agents/opencode"} + "gemini": "@local/agentenv-framework/agents/gemini", "opencode": "@local/agentenv-framework/agents/opencode", + "letta-code": "@local/agentenv-framework/agents/letta-code"} assert table.target("claude-code").target_id == get_config().get_default_a2a_agent_id() From 12d9f236a075d260bc93154a0d98808044386ec1 Mon Sep 17 00:00:00 2001 From: Letta Integration <300689746+letta-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:31:30 +0000 Subject: [PATCH 04/13] build(agents): register Letta Code in image release checks --- .github/dependabot.yml | 1 + .github/scripts/infra_build.py | 3 +++ tst/unit/test_infra_images_script.py | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fde20cd99..9b35b3953 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -43,6 +43,7 @@ updates: - "/src/agent_env/agents/agents/claude-code" - "/src/agent_env/agents/agents/codex" - "/src/agent_env/agents/agents/gemini" + - "/src/agent_env/agents/agents/letta-code" - "/src/agent_env/agents/agents/opencode" - "/src/agent_env/agents/agents/openhands" - "/src/agent_env/agents/agents/pi" diff --git a/.github/scripts/infra_build.py b/.github/scripts/infra_build.py index 4826777d2..121caa52b 100644 --- a/.github/scripts/infra_build.py +++ b/.github/scripts/infra_build.py @@ -34,6 +34,7 @@ "agentenv-claude-code": "src/agent_env/agents/agents/claude-code/Dockerfile", "agentenv-codex": "src/agent_env/agents/agents/codex/Dockerfile", "agentenv-gemini": "src/agent_env/agents/agents/gemini/Dockerfile", + "agentenv-letta-code": "src/agent_env/agents/agents/letta-code/Dockerfile", "agentenv-opencode": "src/agent_env/agents/agents/opencode/Dockerfile", "agentenv-openhands": "src/agent_env/agents/agents/openhands/Dockerfile", "agentenv-pi": "src/agent_env/agents/agents/pi/Dockerfile", @@ -72,6 +73,8 @@ 'codex --version && mcp-remote --help >/dev/null && python -c "import a2a_server"'], "agentenv-gemini": ["docker", "run", "--rm", "--entrypoint", "sh", "{image}", "-c", 'gemini --version && python -c "import a2a_server"'], + "agentenv-letta-code": ["docker", "run", "--rm", "--entrypoint", "sh", "{image}", "-c", + 'letta --version && python -c "import a2a_server"'], "agentenv-opencode": ["docker", "run", "--rm", "--entrypoint", "sh", "{image}", "-c", 'opencode --version && python -c "import a2a_server"'], "agentenv-openhands": ["docker", "run", "--rm", "--entrypoint", "sh", "{image}", "-c", 'python -c "import a2a_server"'], diff --git a/tst/unit/test_infra_images_script.py b/tst/unit/test_infra_images_script.py index 97c549040..e44825acf 100644 --- a/tst/unit/test_infra_images_script.py +++ b/tst/unit/test_infra_images_script.py @@ -62,8 +62,8 @@ def test_plan_only_lists_the_infra_images_or_the_agents_images(script, monkeypat assert planned == {"infra": [image.name for image in bootstrap.PUBLISHED_IMAGES if image.name not in bundles], "agents": bundles} assert "agentenv-gateway" in planned["infra"] and bundles == [ - "agentenv-claude-code", "agentenv-codex", "agentenv-gemini", "agentenv-opencode", "agentenv-openhands", - "agentenv-pi", "agentenv-proxy", "agentenv-mail-mcp"] + "agentenv-claude-code", "agentenv-codex", "agentenv-gemini", "agentenv-letta-code", "agentenv-opencode", + "agentenv-openhands", "agentenv-pi", "agentenv-proxy", "agentenv-mail-mcp"] def test_plan_missing_lists_the_images_the_registry_lacks_or_refuses_to_show(script, monkeypatch, capsys): From ceea583dd41875db481b40f45d1579f79b220922 Mon Sep 17 00:00:00 2001 From: Letta Integration <300689746+letta-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:31:30 +0000 Subject: [PATCH 05/13] fix(agents): expose MCP schemas as direct Letta Code tools --- .../agents/agents/letta-code/Dockerfile | 5 +-- .../agents/agents/letta-code/README.md | 9 ++--- .../agents/agents/letta-code/install.sh | 2 +- .../agents/letta-code/letta_code_bridge.py | 25 +++++++++----- .../agents/agents/letta-code/mcp_tools.ts | 34 +++++++++++++++++++ tst/unit/agents/letta_code/test_bridge.py | 20 +++++++++-- 6 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 src/agent_env/agents/agents/letta-code/mcp_tools.ts diff --git a/src/agent_env/agents/agents/letta-code/Dockerfile b/src/agent_env/agents/agents/letta-code/Dockerfile index 0c12e79c9..6ead4f69e 100644 --- a/src/agent_env/agents/agents/letta-code/Dockerfile +++ b/src/agent_env/agents/agents/letta-code/Dockerfile @@ -24,10 +24,11 @@ RUN npm ci --ignore-scripts --omit=dev --no-audit --no-fund --loglevel=error \ COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt # install.sh and run.sh are install/v1's: in the image, so they stay in its build context. -COPY a2a_server.py letta_code_bridge.py letta_code_kit.py entrypoint.sh install.sh run.sh ./ +COPY mcp_tools.ts a2a_server.py letta_code_bridge.py letta_code_kit.py entrypoint.sh install.sh run.sh ./ # The CLI's cwd, /app, and its state under /data, fixed so neither follows the uid: Modal runs the image as root. RUN useradd --create-home --uid 10001 agent && mkdir -p /app /data \ - && chown agent:agent /app /data && chmod 0755 entrypoint.sh && chmod -R go+rX /opt/agent + && chown agent:agent /app /data && chmod 0755 entrypoint.sh \ + && find /opt/agent \( ! -perm -044 -o \( -perm /111 ! -perm -011 \) \) -exec chmod go+rX {} + ENV A2A_PORT=8000 PYTHONPATH=/opt/agent HOME=/data XDG_CONFIG_HOME=/data/config XDG_CACHE_HOME=/data/cache \ XDG_DATA_HOME=/data/share XDG_STATE_HOME=/data/state USER agent diff --git a/src/agent_env/agents/agents/letta-code/README.md b/src/agent_env/agents/agents/letta-code/README.md index 4fb6fe9ec..9017d070b 100644 --- a/src/agent_env/agents/agents/letta-code/README.md +++ b/src/agent_env/agents/agents/letta-code/README.md @@ -14,16 +14,17 @@ The bridge connects the local backend with `letta connect openai-compatible`. A requests to `$LITELLM_BASE_URL/v1`, with `LITELLM_API_KEY`, `[model.headers]` (`LITELLM_EXTRA_HEADERS`), and `project_id` / `task_id` as `x-litellm-customer-id` and `x-litellm-tags`. The model defaults to `openai/gpt-4.1-mini`. Endpoint credentials stay at the relay. Registered MCP servers retain their names and headers; -a task role replaces `AgentEnv-Role`. Task instructions explain discovery and the full `mcp____` -call form for the CLI's MCP tools, including peer tools. +a task role replaces `AgentEnv-Role`. Before each turn, `letta mcp tools --full` discovers the exact schemas. A context-local mod registers them as +direct Letta Code tools through `letta.tools.register`; each tool executes `letta mcp call` with its full +`mcp____` name. Peer tools use the same path. | Extension | Implementation | |---|---| | `agent-config/v1` | SDK set/get; `model`, `effort`, `system_prompt`, `output_format`, `project_id`, `task_id`, `timeout_seconds`. Before every turn, task instructions are committed to the existing agent's core memory, which the local backend compiles into its system prompt. JSON schema answers are validated locally. | -| `mcp-config/v1` | SDK registration and listing; servers passed to the context's agent settings each turn, with exact-name discovery instructions. | +| `mcp-config/v1` | SDK registration and listing; servers passed to the context's agent settings each turn, with their discovered schemas exposed as direct tools through the public mods API. | | `skill-config/v1` | Inline and bundle skills installed under `$HOME/.letta/skills/`, copied into each context's home before its turn. | | `trajectory/v1` | Native stream-json events appended live, format `letta-code-events/1`. | -| `snapshot/v1` | Save archives the complete context home, including conversations, agent records, settings and git-backed memory; load stages it and restores it into the target context, rewriting stored home paths and binding the saved conversation. Optional workspace archives restore `/app`. GNU tar changelog captures task-level file increments, including removed roots, using the OpenCode template's implementation. | +| `snapshot/v1` | Save archives the complete context home, including conversations, agent records, settings and git-backed memory; load stages it and restores it into the target context, rewriting stored home paths and binding the saved conversation. Optional workspace archives restore `/app`. The validator reports load/recall as false without its optional Claude Code rubric judge, as it does for the OpenCode baseline; a live cross-context recall probe verifies restore independently. GNU tar changelog captures task-level file increments, including removed roots, using the OpenCode template's implementation. | | `peer-agents/v1` | SDK default; its `peer_agents` MCP server is registered alongside task servers. | | `attribution-probe/v1` | Reports the project and task attribution used by the last CLI turn. | | `install/v1` | Template installer brings checksum-verified Node and uv, the pinned CLI, and a private Python into a container or a Linux host with glibc. Installs git if missing. | diff --git a/src/agent_env/agents/agents/letta-code/install.sh b/src/agent_env/agents/agents/letta-code/install.sh index 1301e9906..7197ad116 100644 --- a/src/agent_env/agents/agents/letta-code/install.sh +++ b/src/agent_env/agents/agents/letta-code/install.sh @@ -24,7 +24,7 @@ START_TIMEOUT_S=120 STOP_TIMEOUT_S=10 dockerfile_arg() { sed -n "s/^ARG $1=//p" "$here/Dockerfile"; } NODE_VERSION=$(dockerfile_arg NODE_VERSION) -AGENT_FILES="a2a_server.py letta_code_bridge.py letta_code_kit.py requirements.txt run.sh" +AGENT_FILES="mcp_tools.ts a2a_server.py letta_code_bridge.py letta_code_kit.py requirements.txt run.sh" # put SRC DEST replaces DEST by a rename, so a binary an earlier install's agent still runs can be replaced; put_dir SRC # DEST copies a folder to DEST, which isn't there. diff --git a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py index 4f3bf2d77..0059ce5ec 100644 --- a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py +++ b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py @@ -286,9 +286,11 @@ async def forward(request): if name.lower() not in {"content-length", "transfer-encoding", "connection"} }) await response.prepare(request) - async for chunk in upstream.content.iter_any(): - await response.write(chunk) - await response.write_eof() + # SSE consumers commonly close immediately after the terminal event. + with contextlib.suppress(ConnectionResetError): + async for chunk in upstream.content.iter_any(): + await response.write(chunk) + await response.write_eof() return response app = web.Application(client_max_size=100 * 1024**2) app.router.add_route("*", "/{path:.*}", forward) @@ -316,11 +318,8 @@ def _configure_agent(home: Path, agent_id: str, servers: dict, role: str | None, memory.mkdir(parents=True, exist_ok=True) instructions = system_prompt or "" if servers: - instructions += ("\nMCP tools are available through the Letta CLI. Before calling any MCP tool, run " - "`letta mcp tools --full --agent " + agent_id + "` to discover exact names and schemas. " - "Use the full mcp____ name returned by discovery, never a bare tool name. " - "Call `letta mcp call --agent " + agent_id + " --args ''`. " - "Use these tools to act on connected services and peer agents.\n") + instructions += ("\nUse the connected MCP tools directly for services and peer agents. " + "They are named mcp____. These services require no repository setup.\n") target = "task_instructions.md" if (memory / "MEMORY.md").exists() else "system/task_instructions.md" path = memory / target path.parent.mkdir(parents=True, exist_ok=True) @@ -348,6 +347,14 @@ def _configure_agent(home: Path, agent_id: str, servers: dict, role: str | None, settings_path.write_text(json.dumps(settings)) +def _install_mcp_tools(home: Path, agent_id: str, tools: list[dict]) -> None: + """A context-local mod registers the CLI-discovered schemas without renaming their tools.""" + directory = home / ".letta" + (directory / "mods").mkdir(parents=True, exist_ok=True) + (directory / "agentenv-mcp-tools.json").write_text(json.dumps({"agentId": agent_id, "tools": tools})) + shutil.copyfile(Path(__file__).with_name("mcp_tools.ts"), directory / "mods/agentenv-mcp.ts") + + async def run_letta_code( prompt: str, context_id: str, session_id: str | None = None, model: str | None = None, mcp_servers: dict[str, dict] | None = None, system_prompt: str | None = None, @@ -383,6 +390,8 @@ async def _turn(prompt, context_id, session_id, model, mcp_servers, system_promp agent_id = agent["id"] agent_file.write_text(json.dumps({"id": agent_id})) _configure_agent(home, agent_id, mcp_servers or {}, role, system_prompt) + tools = json.loads(await _command(env, "mcp", "tools", "--full", "--agent", agent_id)) if mcp_servers else [] + _install_mcp_tools(home, agent_id, tools) skills = Path(os.environ.get("HOME", "/data")) / ".letta/skills" if skills.exists(): shutil.copytree(skills, home / ".letta/skills", dirs_exist_ok=True) diff --git a/src/agent_env/agents/agents/letta-code/mcp_tools.ts b/src/agent_env/agents/agents/letta-code/mcp_tools.ts new file mode 100644 index 000000000..688c910ba --- /dev/null +++ b/src/agent_env/agents/agents/letta-code/mcp_tools.ts @@ -0,0 +1,34 @@ +/** Expose the registered MCP schemas as native Letta Code tools through its public mods API. */ +import { execFile } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +export default function activate(letta) { + const { agentId, tools } = JSON.parse( + readFileSync(join(homedir(), ".letta", "agentenv-mcp-tools.json"), "utf8"), + ); + const dispose = tools.map((tool) => letta.tools.register({ + name: tool.name, + description: tool.description || tool.name, + parameters: tool.inputSchema, + requiresApproval: false, + parallelSafe: false, + run(ctx) { + return new Promise((resolve) => { + execFile("letta", ["--backend", "local", "mcp", "call", tool.name, + "--agent", agentId, "--args", JSON.stringify(ctx.args)], + { signal: ctx.signal, timeout: 120000, maxBuffer: 16 * 1024 * 1024 }, + (error, stdout, stderr) => { + let result; + try { result = JSON.parse(stdout); } catch { /* CLI startup errors have only stderr. */ } + resolve({ + content: result ? JSON.stringify(result) : (stderr || error?.message || stdout), + isError: Boolean(error) || result?.isError === true, + }); + }); + }); + }, + })); + return () => dispose.forEach((remove) => remove()); +} diff --git a/tst/unit/agents/letta_code/test_bridge.py b/tst/unit/agents/letta_code/test_bridge.py index bb4c561fa..31d6200c5 100644 --- a/tst/unit/agents/letta_code/test_bridge.py +++ b/tst/unit/agents/letta_code/test_bridge.py @@ -72,7 +72,8 @@ def test_native_events_track_conversation_tools_and_stderr(): @pytest.mark.asyncio -async def test_cancel_kills_the_turn_process(monkeypatch, tmp_path): +@pytest.mark.parametrize("session_id", [None, "local-conv-7"]) +async def test_cancel_kills_the_turn_process(monkeypatch, tmp_path, session_id): monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path / "sessions")) agent = a2a_server.LettaCodeAgent() app = a2a_server.build_app(agent) @@ -102,15 +103,30 @@ async def relay(*args): monkeypatch.setattr(bridge, "model_relay", relay) async def spawn(*args, **kwargs): + if session_id: + assert "--conversation" in args and "--agent" not in args + else: + assert "--agent" in args and "--new" in args return Proc() monkeypatch.setattr(bridge, "_command", command) monkeypatch.setattr(bridge, "_configure_agent", lambda *args: None) monkeypatch.setattr(bridge.asyncio, "create_subprocess_exec", spawn) monkeypatch.setattr(bridge, "_stream_cli", stream) - turn = asyncio.create_task(bridge.run_letta_code("wait", "ctx", workspace=str(tmp_path))) + turn = asyncio.create_task(bridge.run_letta_code("wait", "ctx", session_id=session_id, workspace=str(tmp_path))) await asyncio.wait_for(started.wait(), 5) turn.cancel() with pytest.raises(asyncio.CancelledError): await turn assert stopped.is_set() and not bridge._RUNNING + + +def test_direct_mcp_catalog_replaces_previous_context_tools(tmp_path): + bridge._install_mcp_tools(tmp_path, "agent-local-1", [{ + "name": "mcp__mail__search_emails", "description": "Search mail", "inputSchema": {"type": "object"}, + }]) + catalog = tmp_path / ".letta/agentenv-mcp-tools.json" + assert json.loads(catalog.read_text())["tools"][0]["name"] == "mcp__mail__search_emails" + assert (tmp_path / ".letta/mods/agentenv-mcp.ts").exists() + bridge._install_mcp_tools(tmp_path, "agent-local-1", []) + assert json.loads(catalog.read_text())["tools"] == [] From 90c27c0becd9e88b99629259a46b579d08b9e0e1 Mon Sep 17 00:00:00 2001 From: Letta Integration <300689746+letta-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:51:06 +0000 Subject: [PATCH 06/13] Add optional Letta Cloud backend with secret-backed agent config --- .agentenv/config.example.toml | 4 + src/agent_env/agents/README.md | 7 +- .../agents/agents/letta-code/README.md | 63 +++- .../agents/agents/letta-code/a2a_server.py | 5 + .../agents/letta-code/letta_code_bridge.py | 141 +++++++-- src/agent_env/config/describe.py | 2 +- src/agent_env/config/runtime.py | 13 +- .../task_step/task_steps/deploy_agent.py | 7 + tst/unit/agents/letta_code/test_cloud.py | 287 ++++++++++++++++++ 9 files changed, 500 insertions(+), 29 deletions(-) create mode 100644 tst/unit/agents/letta_code/test_cloud.py diff --git a/.agentenv/config.example.toml b/.agentenv/config.example.toml index 6146dee22..41a741e6c 100644 --- a/.agentenv/config.example.toml +++ b/.agentenv/config.example.toml @@ -38,6 +38,10 @@ agent_default = "local" # agents too # [agents] # default_a2a_agent_id = "my-agent" # what a deploy_agent step that names no agent deploys; by default the # # agents bundle's Claude Code, @local/agentenv-framework/agents/claude-code +# Per-deployed-agent agent-config fields (negotiated with its Agent Card): +# [agents.config.default-agent] +# letta_api_key = "secret:LETTA_API_KEY" # optional cloud mode for letta-code +# letta_base_url = "https://api.letta.com" # [model] # base_url = "http://localhost:4000" # a model proxy on this machine; local containers reach it as host.docker.internal diff --git a/src/agent_env/agents/README.md b/src/agent_env/agents/README.md index 720aa4bf0..f9a37b744 100644 --- a/src/agent_env/agents/README.md +++ b/src/agent_env/agents/README.md @@ -62,10 +62,11 @@ OpenAI-compatible one, `/v1/chat/completions`, and takes its model from th with glibc. Its folder's `README.md` covers the extensions it implements. `agents/letta-code/` runs [Letta Code](https://github.com/letta-ai/letta-code) behind the protocol's A2A agent -framework: each turn is one `letta --backend local -p --output-format stream-json` run, resumed in the context's -conversation, with a separate local agent and memory per context. Name it as `letta-code`, its alias, or by its id, +framework: each turn runs the CLI in headless mode, resumed in the context's conversation. Local mode keeps separate agent state per context; an optional secret-backed agent-config key +selects Letta Cloud with persistent server-side memory. Name it as `letta-code`, its alias, or by its id, `@local/agentenv-framework/agents/letta-code`. It calls the `[model]` endpoint as an OpenAI-compatible one through -a relay that adds task attribution, and takes its model from the prompt, else `openai/gpt-4.1-mini`. It declares +a local relay that adds task attribution, or a cloud BYOK provider with the header limitations documented in +its README, and takes its model from the prompt, else `openai/gpt-4.1-mini`. It declares `install/v1` too, into a container or onto a Linux host with glibc. Its folder's `README.md` covers the extensions it implements. Run its smoke task with `agent-env run agents --task letta-code-smoke`. diff --git a/src/agent_env/agents/agents/letta-code/README.md b/src/agent_env/agents/agents/letta-code/README.md index 9017d070b..ec04bcd3e 100644 --- a/src/agent_env/agents/agents/letta-code/README.md +++ b/src/agent_env/agents/agents/letta-code/README.md @@ -5,12 +5,12 @@ in the `agents` bundle as `@local/agentenv-framework/agents/letta-code`, with al is the whole build context. The Dockerfile pins the Python base by digest and Node by version and checksum; `package-lock.json` pins `@letta-ai/letta-code` to 0.34.25. -Each turn runs `letta --backend local -p --output-format stream-json --yolo`. The bridge creates an agent with +By default each turn runs `letta --backend local -p --output-format stream-json --yolo`. The bridge creates a local agent with `letta agents create`; the first turn uses `--agent --new`, and subsequent turns use `--conversation `. Each context has a separate home under `$HOME/sessions/`. Different contexts run concurrently; turns in one context and snapshot operations are synchronized. -The bridge connects the local backend with `letta connect openai-compatible`. A per-turn loopback relay forwards +In local mode, the bridge connects the backend with `letta connect openai-compatible`. A per-turn loopback relay forwards requests to `$LITELLM_BASE_URL/v1`, with `LITELLM_API_KEY`, `[model.headers]` (`LITELLM_EXTRA_HEADERS`), and `project_id` / `task_id` as `x-litellm-customer-id` and `x-litellm-tags`. The model defaults to `openai/gpt-4.1-mini`. Endpoint credentials stay at the relay. Registered MCP servers retain their names and headers; @@ -20,13 +20,13 @@ direct Letta Code tools through `letta.tools.register`; each tool executes `lett | Extension | Implementation | |---|---| -| `agent-config/v1` | SDK set/get; `model`, `effort`, `system_prompt`, `output_format`, `project_id`, `task_id`, `timeout_seconds`. Before every turn, task instructions are committed to the existing agent's core memory, which the local backend compiles into its system prompt. JSON schema answers are validated locally. | +| `agent-config/v1` | SDK set/get; `model`, `effort`, `system_prompt`, `output_format`, `project_id`, `task_id`, `timeout_seconds`, write-only `letta_api_key`, and `letta_base_url`. Local task instructions are committed to core memory; cloud task instructions are prepended to each turn. JSON schema answers are validated locally. | | `mcp-config/v1` | SDK registration and listing; servers passed to the context's agent settings each turn, with their discovered schemas exposed as direct tools through the public mods API. | | `skill-config/v1` | Inline and bundle skills installed under `$HOME/.letta/skills/`, copied into each context's home before its turn. | | `trajectory/v1` | Native stream-json events appended live, format `letta-code-events/1`. | -| `snapshot/v1` | Save archives the complete context home, including conversations, agent records, settings and git-backed memory; load stages it and restores it into the target context, rewriting stored home paths and binding the saved conversation. Optional workspace archives restore `/app`. The validator reports load/recall as false without its optional Claude Code rubric judge, as it does for the OpenCode baseline; a live cross-context recall probe verifies restore independently. GNU tar changelog captures task-level file increments, including removed roots, using the OpenCode template's implementation. | +| `snapshot/v1` | Local save/load archives and restores the context home, conversations and git-backed memory, rewriting home paths. Cloud save/load records and rebinds the agent ID, conversation ID and base URL; it references live server memory rather than copying or rolling it back. Both support optional workspace archives. GNU tar changelog captures completed-turn file increments, including removed roots. | | `peer-agents/v1` | SDK default; its `peer_agents` MCP server is registered alongside task servers. | -| `attribution-probe/v1` | Reports the project and task attribution used by the last CLI turn. | +| `attribution-probe/v1` | Reports attribution forwarded by the local relay. Cloud provider calls cannot carry per-turn attribution; they do not update this probe. | | `install/v1` | Template installer brings checksum-verified Node and uv, the pinned CLI, and a private Python into a container or a Linux host with glibc. Installs git if missing. | | `staging/v1` | SDK default. | @@ -37,3 +37,56 @@ The end of CLI stderr is included in failures. Cancellation and shutdown kill th Run `agent-env run agents --task letta-code-smoke` for the shell smoke task and `agent-env a2a-agent validate --id letta-code --sandbox local` for the repository's validator. + +## Backends + +Local mode is the default when `letta_api_key` is absent or empty. Configure cloud mode in +`.agentenv/config.toml`, using the **deployed agent name** (`default-agent` in the shipped smoke task): + +```toml +[agents.config.default-agent] +letta_api_key = "secret:LETTA_API_KEY" +letta_base_url = "https://api.letta.com" # optional, this is the default + +[model] +base_url = "https://your-public-gateway.example" +api_key = "secret:MODEL_API_KEY" +default = "openai/gpt-4.1-mini" +``` + +`[agents.config.]` supplies negotiated `agent-config/v1` fields at deployment. +`secret:` references resolve through `[stores.secret]`; `env:` references also work. The default +secret store reads the named environment variable, but secrets never belong literally in TOML. +The key is write-only and omitted from agent-config reads. This uses the repository's config file +and secret store instead of introducing a new environment switch. `agent.toml` remains optional; +putting a key in `[default_env_vars]` there would persist credentials with the agent definition. +The bridge sets the CLI's existing authentication variables internally and ignores inherited +`LETTA_*` settings so unrelated CLI sessions cannot select this run's backend or identity. + +With a key, the bridge omits `--backend local` and uses the CLI's default cloud backend. It +registers an OpenAI-compatible BYOK provider using the same `/v1/providers` API as the pinned +CLI's `connect` implementation (`provider_type = "openai"`). The provider points directly to +`$LITELLM_BASE_URL/v1` and stores the task endpoint's key on Letta Cloud. Both the agent model and +compaction model use `/`, retaining gateway routing. The endpoint must +be reachable from Letta Cloud; a sandbox loopback relay or private gateway cannot serve it. +A deterministic provider name hashes the context, endpoint and key, so sequential runs reuse it; +key rotation creates a new provider. Providers remain on the account for reuse. + +**Header limitation:** the pinned provider API supports a base URL and API key, but no arbitrary +model-call headers. Cloud mode therefore rejects nonempty `[model.headers]` instead of silently +dropping required headers. It cannot forward per-turn `x-litellm-customer-id` / `x-litellm-tags`, +so Scale gateway routing still applies but project/task header attribution does not. Use local +mode when those headers are required. The attribution probe does not claim cloud forwarding. + +An agent's stable name is `agentenv-` plus the first 32 hex characters of SHA-256 of the A2A +context ID. Cloud mode looks it up before creating it, preserving its server-side memory across +runs that use the same context. A new context starts a new agent; snapshot load can explicitly +bind another context to the saved agent and conversation. Without a saved conversation ID, +a run starts a new conversation on the reused agent. Keep independent concurrent runs on +separate contexts; cross-process creation of the same context is not serialized. + +Cloud snapshots contain only IDs and the base URL, never the cloud key or server memory. +Restore requires a configured key with access to the original agent on the same base URL. +It resumes the saved conversation against the agent's **current** memory: no offline restore, +point-in-time rollback, account migration or cloning is provided. Workspace save/load remains +available. Local snapshots retain their full state-copy behavior. diff --git a/src/agent_env/agents/agents/letta-code/a2a_server.py b/src/agent_env/agents/agents/letta-code/a2a_server.py index eb214936a..622db785f 100644 --- a/src/agent_env/agents/agents/letta-code/a2a_server.py +++ b/src/agent_env/agents/agents/letta-code/a2a_server.py @@ -54,6 +54,7 @@ TaskResult, TextPart, Usage, + WriteOnly, a2a_agent, download, enable, @@ -130,6 +131,8 @@ def _changelog_backup_exclusions() -> list[str]: class LettaCodeConfig(AgentConfig): + letta_api_key: WriteOnly[str | None] = None + letta_base_url: str = "https://api.letta.com" model: str | None = None effort: str | None = None system_prompt: str | None = None @@ -342,6 +345,8 @@ def record(event: dict) -> None: context_id=context_id, session_id=session_id, model=request.config.model, + letta_api_key=request.config.letta_api_key, + letta_base_url=request.config.letta_base_url, effort=request.config.effort, mcp_servers=dict(request.mcp_servers), system_prompt=request.config.system_prompt, diff --git a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py index 0059ce5ec..b6b546056 100644 --- a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py +++ b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py @@ -1,4 +1,4 @@ -"""Letta Code local-backend bridge, with isolated context state and a per-turn model relay.""" +"""Letta Code bridge with local context state or persistent cloud agents.""" from __future__ import annotations import asyncio @@ -52,11 +52,17 @@ def context_data_dir(context_id: str) -> Path: return Path(SESSIONS_ROOT) / ctx_hash -def _context_env(context_id: str) -> dict[str, str]: +def _context_env(context_id: str, api_key: str | None = None, base_url: str = "https://api.letta.com") -> dict[str, str]: home = context_data_dir(context_id) home.mkdir(parents=True, exist_ok=True) - return {**os.environ, "HOME": str(home), "LETTA_LOCAL_BACKEND_DIR": str(home / ".letta/lc-local-backend"), - "LETTA_LOCAL_BACKEND_EXPERIMENTAL": "1"} + environ = {key: value for key, value in os.environ.items() + if not key.startswith("LETTA_")} + if api_key: + environ.update(LETTA_API_KEY=api_key, LETTA_BASE_URL=base_url, LETTA_BACKEND="api") + else: + environ["LETTA_BACKEND"] = "local" + return {**environ, "HOME": str(home), "LETTA_LOCAL_BACKEND_DIR": str(home / ".letta/lc-local-backend"), + "LETTA_LOCAL_BACKEND_EXPERIMENTAL": "0" if api_key else "1"} async def _communicate(*cmd: str, env: Mapping[str, str], timeout_s: int) -> tuple[int, bytes, bytes]: @@ -83,7 +89,13 @@ async def export_session(context_id: str, session_id: str) -> bytes: (root / "snapshot-session.json").write_text(json.dumps({"session_id": session_id})) buffer = io.BytesIO() with tarfile.open(fileobj=buffer, mode="w:gz") as archive: - archive.add(root, arcname="state") + agent_file = root / "agent-id.json" + cloud = agent_file.exists() and json.loads(agent_file.read_text()).get("backend") == "cloud" + if cloud: + for name in ("agent-id.json", "context.json", "snapshot-session.json"): + archive.add(root / name, arcname="state/" + name) + else: + archive.add(root, arcname="state") return buffer.getvalue() @@ -305,13 +317,82 @@ async def forward(request): await runner.cleanup() +def _cli(env: Mapping[str, str]) -> list[str]: + return ["letta"] if env.get("LETTA_API_KEY") else ["letta", "--backend", "local"] + + +@asynccontextmanager +async def _provider(env, context_id, model, attribution): + if not env.get("LETTA_API_KEY"): + async with model_relay(provider_headers(os.environ, attribution)) as relay: + await _command(env, "connect", "openai-compatible", "--base-url", relay, "--api-key", "relay") + yield "openai-compatible/" + model + return + # The cloud provider API used by CLI 0.34.25 carries a URL and key, but no arbitrary headers. + # Never silently discard endpoint headers (which may themselves be authentication). + if model_headers(os.environ.get(MODEL_HEADERS_ENV)): + raise ValueError("Letta Cloud BYOK cannot forward [model.headers]; use local mode for this endpoint") + base = _base_url(os.environ) + if not os.environ.get("LITELLM_BASE_URL"): + raise ValueError("Cloud mode requires the task's [model] endpoint") + # A provider belongs to a context and endpoint/key combination. Credential rotation gets a new + # provider without changing another context's in-flight calls. No raw credential is persisted locally. + identity = json.dumps([context_id, base, os.environ.get("LITELLM_API_KEY", "")]) + name = "agentenv-" + hashlib.sha256(identity.encode()).hexdigest()[:32] + async with ClientSession(headers={"Authorization": "Bearer " + env["LETTA_API_KEY"]}, + timeout=ClientTimeout(total=_CLI_TIMEOUT_S)) as client: + url = env["LETTA_BASE_URL"].rstrip("/") + "/v1/providers" + async with client.get(url) as response: + response.raise_for_status() + providers = await response.json() + if not any(provider["name"] == name for provider in providers): + async with client.post(url, json={"name": name, "provider_type": "openai", "base_url": base, + "api_key": os.environ.get("LITELLM_API_KEY", "")}) as response: + # A second process may have created the same deterministic provider. + if response.status == 409: + async with client.get(url) as retry: + retry.raise_for_status() + if not any(item["name"] == name for item in await retry.json()): + response.raise_for_status() + else: + response.raise_for_status() + yield name + "/" + model + + async def _command(env: dict[str, str], *args: str) -> bytes: - code, out, err = await _communicate("letta", "--backend", "local", *args, env=env, timeout_s=120) + code, out, err = await _communicate(*_cli(env), *args, env=env, timeout_s=120) if code: raise RuntimeError(f"Letta Code {args[0]} failed: {err.decode(errors='replace')[-ERROR_MESSAGE_LIMIT:]}") return out +async def _cloud_agent(env, context_id, handle, agent_id=None): + """Resolve the context on the server and bind both inference and compaction to BYOK.""" + base = env["LETTA_BASE_URL"].rstrip("/") + "/v1/agents" + name = "agentenv-" + hashlib.sha256(context_id.encode()).hexdigest()[:32] + async with ClientSession(headers={"Authorization": "Bearer " + env["LETTA_API_KEY"]}, + timeout=ClientTimeout(total=_CLI_TIMEOUT_S)) as client: + if not agent_id: + async with client.get(base, params={"name": name}) as response: + response.raise_for_status() + agents = await response.json() + if agents: + agent_id = agents[0]["id"] + config = {"model": handle, "compaction_settings": {"model": handle}} + if agent_id: + async with client.patch(base + "/" + agent_id, json=config) as response: + response.raise_for_status() + else: + async with client.post(base, json={ + **config, "name": name, "tags": ["origin:letta-code", "git-memory-enabled"], + "include_base_tools": False, "include_base_tool_rules": False, + "tools": [], "initial_message_sequence": [], + }) as response: + response.raise_for_status() + agent_id = (await response.json())["id"] + return agent_id + + def _configure_agent(home: Path, agent_id: str, servers: dict, role: str | None, system_prompt: str | None): # Core memory's system/ files are compiled into the existing agent's system prompt on every run. memory = home / ".letta/lc-local-backend/memfs" / agent_id / "memory" @@ -331,12 +412,16 @@ def _configure_agent(home: Path, agent_id: str, servers: dict, role: str | None, "-m", "Update task instructions", "--", target]): subprocess.run(["git", *args], cwd=memory, check=True, capture_output=True) + _configure_settings(home, agent_id, servers, role) + + +def _configure_settings(home, agent_id, servers, role, base_url=None): settings_path = home / ".letta/settings.json" settings = json.loads(settings_path.read_text()) if settings_path.exists() else {} agents = settings.setdefault("agents", []) matching = [agent for agent in agents if agent.get("agentId") == agent_id] if not matching: - matching = [{"agentId": agent_id, "baseUrl": "local:" + str(home / ".letta/lc-local-backend")}] + matching = [{"agentId": agent_id, "baseUrl": base_url or "local:" + str(home / ".letta/lc-local-backend")}] agents.extend(matching) for agent in matching: agent["mcpServers"] = [ @@ -360,36 +445,52 @@ async def run_letta_code( mcp_servers: dict[str, dict] | None = None, system_prompt: str | None = None, output_format: dict | None = None, timeout_s: int = 600, project_id: str | None = None, task_id: str | None = None, role: str | None = None, attachment_paths: list[str] | None = None, + letta_api_key: str | None = None, letta_base_url: str = "https://api.letta.com", effort: str | None = None, event_callback: Callable[[dict], None] | None = None, workspace: str = "/app", ) -> LettaCodeResult: """A bounded CLI turn, including provider setup; cancellation kills the process tree.""" try: async with asyncio.timeout(timeout_s): return await _turn(prompt, context_id, session_id, model, mcp_servers, system_prompt, output_format, - project_id, task_id, role, attachment_paths, effort, event_callback, workspace) + project_id, task_id, role, attachment_paths, effort, event_callback, workspace, + letta_api_key, letta_base_url) except TimeoutError: return LettaCodeResult(response="", tool_call_count=0, session_id=session_id, error=f"Letta Code timed out after {timeout_s}s") async def _turn(prompt, context_id, session_id, model, mcp_servers, system_prompt, output_format, - project_id, task_id, role, attachment_paths, effort, event_callback, workspace): + project_id, task_id, role, attachment_paths, effort, event_callback, workspace, + letta_api_key=None, letta_base_url="https://api.letta.com"): model = model or DEFAULT_MODEL - handle = "openai-compatible/" + model - env = _context_env(context_id) + env = _context_env(context_id, letta_api_key, letta_base_url) home = Path(env["HOME"]) (home / "context.json").write_text(json.dumps({"home": str(home)})) attribution = _attribution_headers(project_id, task_id) - async with model_relay(provider_headers(os.environ, attribution)) as relay: - await _command(env, "connect", "openai-compatible", "--base-url", relay, "--api-key", "relay") + async with _provider(env, context_id, model, attribution) as handle: agent_file = home / "agent-id.json" if agent_file.exists(): - agent_id = json.loads(agent_file.read_text())["id"] + saved = json.loads(agent_file.read_text()) + if (saved.get("backend", "local") == "cloud") != bool(letta_api_key): + raise ValueError("Context backend differs from the configured backend") + if letta_api_key and saved.get("base_url") != letta_base_url: + raise ValueError("Cloud snapshot belongs to another Letta base URL") + agent_id = saved["id"] else: + agent_id = None + if letta_api_key: + agent_id = await _cloud_agent(env, context_id, handle, agent_id) + elif agent_id is None: agent = json.loads(await _command(env, "agents", "create", "--model", handle)) agent_id = agent["id"] - agent_file.write_text(json.dumps({"id": agent_id})) - _configure_agent(home, agent_id, mcp_servers or {}, role, system_prompt) + agent_file.write_text(json.dumps({"id": agent_id, "backend": "cloud" if letta_api_key else "local", + "base_url": letta_base_url if letta_api_key else None})) + if letta_api_key: + _configure_settings(home, agent_id, mcp_servers or {}, role, letta_base_url) + if system_prompt: + prompt = system_prompt + "\n\n" + prompt + else: + _configure_agent(home, agent_id, mcp_servers or {}, role, system_prompt) tools = json.loads(await _command(env, "mcp", "tools", "--full", "--agent", agent_id)) if mcp_servers else [] _install_mcp_tools(home, agent_id, tools) skills = Path(os.environ.get("HOME", "/data")) / ".letta/skills" @@ -402,7 +503,7 @@ async def _turn(prompt, context_id, session_id, model, mcp_servers, system_promp prompt += "\nReturn only JSON matching this schema: " + json.dumps(schema) for path in attachment_paths or []: prompt += "\nRead the attached file at " + path - cmd = ["letta", "--backend", "local", "-p", prompt, "--output-format", "stream-json", "--yolo", + cmd = [*_cli(env), "-p", prompt, "--output-format", "stream-json", "--yolo", "--model", handle, "--toolset", "default"] cmd += ["--conversation", session_id] if session_id else ["--agent", agent_id, "--new"] if effort: @@ -414,7 +515,11 @@ async def _turn(prompt, context_id, session_id, model, mcp_servers, system_promp )) _RUNNING.add(proc) parser = _EventParser(event_callback) - _record_attribution(project_id, task_id) + if not letta_api_key: + _record_attribution(project_id, task_id) + else: + with _attribution_lock: + _last_attribution.update(project_id=None, task_id=None, last_seen_at_utc=None) try: stderr = await _stream_cli(proc, parser.on_line) except BaseException: diff --git a/src/agent_env/config/describe.py b/src/agent_env/config/describe.py index 1445a0b3c..35eb44cf8 100644 --- a/src/agent_env/config/describe.py +++ b/src/agent_env/config/describe.py @@ -276,7 +276,7 @@ class FileSection: _FILE_SECTIONS: tuple[FileSection, ...] = ( FileSection("model", ("model",), "config.runtime", dict.fromkeys(f.name for f in fields(ModelConfig))), FileSection("conversations", ("conversations",), "config.runtime", _resolved_keys("conversations")), - FileSection("agents", ("agents",), "config.runtime", _resolved_keys("agents"), validated_agents_section), + FileSection("agents", ("agents",), "config.runtime", {**_resolved_keys("agents"), "config": Named(None)}, validated_agents_section), FileSection("sandbox", ("sandbox",), "providers.sandbox_providers.sandbox_provider", {"default": None, "agent_default": None, "attribution": None, "providers": Named(_SEAM)}), FileSection("state", ("state",), "providers.env_state.env_state_provider", {"providers": Named(_SEAM)}), diff --git a/src/agent_env/config/runtime.py b/src/agent_env/config/runtime.py index 952d931df..a7a9972e1 100644 --- a/src/agent_env/config/runtime.py +++ b/src/agent_env/config/runtime.py @@ -96,12 +96,16 @@ def _validated_a2a_agent_id(value: object, source: str) -> str: def validated_agents_section(section: Mapping[str, Any]) -> Mapping[str, Any]: """The ``[agents]`` table, checked as the reader checks it; ``config show`` runs this too.""" - unknown = set(section) - {_AGENTS_CONFIG_KEY} + unknown = set(section) - {_AGENTS_CONFIG_KEY, "config"} if unknown: raise ConfigError( f"[{_AGENTS_CONFIG_SECTION}] has unknown keys {sorted(unknown)}; " - f"allowed: ['{_AGENTS_CONFIG_KEY}']" + f"allowed: ['{_AGENTS_CONFIG_KEY}', 'config']" ) + if not isinstance(section.get("config", {}), dict): + raise ConfigError("[agents.config] must be a table keyed by deployed agent name") + if any(not isinstance(fields, dict) for fields in section.get("config", {}).values()): + raise ConfigError("Each [agents.config.] must be a table of agent-config fields") if _AGENTS_CONFIG_KEY in section: _validated_a2a_agent_id( section[_AGENTS_CONFIG_KEY], f"[{_AGENTS_CONFIG_SECTION}] {_AGENTS_CONFIG_KEY}" @@ -384,6 +388,11 @@ def get_default_human_a2a_url(self) -> str: return _validated_human_a2a_url(resolved, key.file_label) raise ConfigError(f"No human-A2A URL configured: set [{_HUMAN_A2A_CONFIG_SECTION}] {_HUMAN_A2A_CONFIG_KEY} or AGENT_ENV_HUMAN_A2A_URL") + def get_agent_config(self, agent_name: str) -> dict[str, Any]: + """Negotiated extension fields for a deployed name, with secret references resolved.""" + section = validated_agents_section(self.section(_AGENTS_CONFIG_SECTION)) + return self.resolve_reference(section.get("config", {}).get(agent_name, {})) + def get_default_a2a_agent_id(self) -> str: """The agent a ``deploy_agent`` step without an id deploys: ``configure(default_a2a_agent_id=...)`` > ``[agents] default_a2a_agent_id`` > the agents bundle's Claude Code.""" diff --git a/src/agent_env/task_step/task_steps/deploy_agent.py b/src/agent_env/task_step/task_steps/deploy_agent.py index f2b3728b6..8290ceed3 100644 --- a/src/agent_env/task_step/task_steps/deploy_agent.py +++ b/src/agent_env/task_step/task_steps/deploy_agent.py @@ -479,6 +479,13 @@ async def _execute(self, context: TaskStepContext, deployed_agents: list) -> Tas card.update(identity.fields) logger.info(f"Stamped card identity on '{self.agent_name}': {sorted(identity.fields)}") + # Per-agent extension config uses the same secret references as model credentials. + configured = config.get_agent_config(self.agent_name) + if configured: + negotiated = A2AAgent.negotiate_agent_config(card, configured) + if negotiated: + await protocol.post_agent_config(a2a_url + negotiated.endpoint, negotiated.fields) + default_model = config.get_model_for_role("agent") if default_model and not context.default_agent_model: context.default_agent_model = default_model diff --git a/tst/unit/agents/letta_code/test_cloud.py b/tst/unit/agents/letta_code/test_cloud.py new file mode 100644 index 000000000..ef7152aec --- /dev/null +++ b/tst/unit/agents/letta_code/test_cloud.py @@ -0,0 +1,287 @@ +"""Cloud selection, provider reuse, CLI execution and reference-only snapshots.""" +import json +from contextlib import asynccontextmanager + +import pytest + +from tst.unit.agents.letta_code.agent import a2a_server, letta_code_bridge as bridge + + +@pytest.mark.parametrize("key", [None, "cloud-secret"]) +def test_backend_is_selected_by_agent_config_not_inherited_credentials(monkeypatch, tmp_path, key): + monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path)) + monkeypatch.setenv("LETTA_API_KEY", "unrelated-shell-key") + monkeypatch.setenv("LETTA_BASE_URL", "https://unrelated.example") + monkeypatch.setenv("LETTA_CODE_AGENT_ROLE", "subagent") + monkeypatch.setenv("LETTA_LOCAL_BACKEND_EXPERIMENTAL", "1") + env = bridge._context_env("ctx", key, "https://cloud.example") + assert "LETTA_CODE_AGENT_ROLE" not in env + assert env["LETTA_LOCAL_BACKEND_EXPERIMENTAL"] == ("0" if key else "1") + assert env["LETTA_BACKEND"] == ("api" if key else "local") + if key: + assert env["LETTA_API_KEY"] == key and env["LETTA_BASE_URL"] == "https://cloud.example" + assert bridge._cli(env) == ["letta"] + else: + assert "LETTA_API_KEY" not in env and "LETTA_BASE_URL" not in env + assert bridge._cli(env) == ["letta", "--backend", "local"] + + +def test_key_is_write_only_in_agent_config(): + schema = a2a_server.LettaCodeConfig.model_json_schema() + assert schema["properties"]["letta_api_key"]["writeOnly"] + assert a2a_server.LettaCodeConfig().letta_base_url == "https://api.letta.com" + + +@pytest.mark.asyncio +async def test_cloud_provider_registers_gateway_once_and_reuses_it(monkeypatch): + monkeypatch.setenv("LITELLM_BASE_URL", "https://gateway.example") + monkeypatch.setenv("LITELLM_API_KEY", "gateway-key") + monkeypatch.delenv(bridge.MODEL_HEADERS_ENV, raising=False) + providers, writes = [], [] + + class Response: + status = 200 + async def __aenter__(self): + return self + async def __aexit__(self, *args): + pass + def raise_for_status(self): + pass + async def json(self): + return providers + + class Client: + def __init__(self, **kwargs): + assert kwargs["headers"] == {"Authorization": "Bearer cloud-key"} + async def __aenter__(self): + return self + async def __aexit__(self, *args): + pass + def get(self, url): + assert url == "https://cloud.example/v1/providers" + return Response() + def post(self, url, json): + writes.append(json) + providers.append({"name": json["name"]}) + return Response() + + monkeypatch.setattr(bridge, "ClientSession", Client) + env = {"LETTA_API_KEY": "cloud-key", "LETTA_BASE_URL": "https://cloud.example"} + handles = [] + for _ in range(2): + async with bridge._provider(env, "ctx", "openai/gpt-4.1-mini", {}) as handle: + handles.append(handle) + assert handles[0] == handles[1] == writes[0]["name"] + "/openai/gpt-4.1-mini" + assert len(writes) == 1 + assert writes[0]["base_url"] == "https://gateway.example/v1" + assert writes[0]["api_key"] == "gateway-key" and writes[0]["provider_type"] == "openai" + monkeypatch.setenv(bridge.MODEL_HEADERS_ENV, '{"X-Required": "secret"}') + with pytest.raises(ValueError, match=r"\[model.headers\]"): + async with bridge._provider(env, "ctx", "model", {}): + pytest.fail("Must not discard required endpoint headers") + + +@pytest.mark.asyncio +@pytest.mark.parametrize("conversation", [None, "conv-existing"]) +async def test_cloud_turn_resumes_agent_and_builds_cloud_process(monkeypatch, tmp_path, conversation): + monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path)) + commands, processes = [], [] + + @asynccontextmanager + async def provider(env, context, model, attribution): + assert env["LETTA_API_KEY"] == "cloud-key" + assert model == "task-model" + yield "run-provider/task-model" + + async def command(env, *args): + commands.append(args) + assert env["LETTA_BASE_URL"] == "https://cloud.example" + return b'{"items":[{"id":"agent-existing"}]}' if args[:2] == ("agents", "list") else b'{}' + + class Proc: + returncode = 0 + async def spawn(*args, **kwargs): + processes.append((args, kwargs)) + return Proc() + async def stream(proc, on_line): + on_line('{"type":"system","conversation_id":"conv-result"}') + on_line('{"type":"result","subtype":"success","result":"42"}') + return b'' + + async def cloud_agent(env, context, handle, agent_id): + assert context == "ctx" and handle == "run-provider/task-model" + return "agent-existing" + + monkeypatch.setattr(bridge, "_cloud_agent", cloud_agent) + monkeypatch.setattr(bridge, "_provider", provider) + monkeypatch.setattr(bridge, "_command", command) + monkeypatch.setattr(bridge.asyncio, "create_subprocess_exec", spawn) + monkeypatch.setattr(bridge, "_stream_cli", stream) + result = await bridge.run_letta_code("answer", "ctx", model="task-model", session_id=conversation, + letta_api_key="cloud-key", letta_base_url="https://cloud.example", + system_prompt="Use the task rules", effort="low", workspace=str(tmp_path)) + assert result.response == "42" and result.session_id == "conv-result" and result.error is None + args, kwargs = processes[0] + assert "--backend" not in args + assert args[args.index("--model") + 1] == "run-provider/task-model" + assert "Use the task rules" in args[args.index("-p") + 1] + assert kwargs["env"]["LETTA_AGENT_ID"] == "agent-existing" + assert "--conversation" in args if conversation else "--new" in args + assert not any(command[:2] == ("agents", "create") for command in commands) + assert any(command[:2] == ("model", "set") for command in commands) + # A fresh server can rebind IDs; credentials and endpoint headers are not in the archive. + home = bridge.context_data_dir("ctx") + (home / "secret-settings").write_text("do not export") + snapshot = tmp_path / "cloud.tgz" + snapshot.write_bytes(await bridge.export_session("ctx", result.session_id)) + assert await bridge.import_session("restored", str(snapshot)) == "conv-result" + restored = bridge.context_data_dir("restored") + assert json.loads((restored / "agent-id.json").read_text())["id"] == "agent-existing" + assert not (restored / "secret-settings").exists() + assert not (restored / ".letta/settings.json").exists() + + +def test_config_resolves_only_the_selected_agent_secrets(monkeypatch): + from agent_env.config.runtime import validated_agents_section + from tst.util.config import config_with_document + + config = config_with_document({"agents": {"config": { + "solver": {"letta_api_key": "secret:cloud", "letta_base_url": "https://cloud.example"}, + "other": {"letta_api_key": "secret:missing"}, + }}}) + requested = [] + def secret(key): + requested.append(key) + return "resolved-key" + monkeypatch.setattr(config, "_resolve_secret", secret) + assert config.get_agent_config("solver") == { + "letta_api_key": "resolved-key", "letta_base_url": "https://cloud.example", + } + assert requested == ["cloud"] + assert config.get_agent_config("absent") == {} + from agent_env.config.errors import ConfigError + with pytest.raises(ConfigError, match="table"): + validated_agents_section({"config": {"solver": "bad"}}) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("bound", [None, "agent-existing"]) +async def test_cloud_agent_reuses_context_and_routes_compaction_through_gateway(monkeypatch, bound): + writes = [] + class Response: + async def __aenter__(self): + return self + async def __aexit__(self, *args): + pass + def raise_for_status(self): + pass + async def json(self): + return [{"id": "agent-existing"}] + class Client: + def __init__(self, **kwargs): + pass + async def __aenter__(self): + return self + async def __aexit__(self, *args): + pass + def get(self, url, params): + assert params["name"].startswith("agentenv-") + return Response() + def patch(self, url, json): + writes.append((url, json)) + return Response() + def post(self, *args, **kwargs): + pytest.fail("Must reuse the existing cloud agent") + monkeypatch.setattr(bridge, "ClientSession", Client) + agent = await bridge._cloud_agent({"LETTA_API_KEY": "key", "LETTA_BASE_URL": "https://cloud.example"}, + "ctx", "provider/model", bound) + assert agent == "agent-existing" + assert writes == [("https://cloud.example/v1/agents/agent-existing", { + "model": "provider/model", "compaction_settings": {"model": "provider/model"}, + })] + + +@pytest.mark.asyncio +async def test_cloud_agent_creation_uses_task_model_and_persistent_memory(monkeypatch): + created = [] + class Response: + def __init__(self, data): + self.data = data + async def __aenter__(self): + return self + async def __aexit__(self, *args): + pass + def raise_for_status(self): + pass + async def json(self): + return self.data + class Client: + def __init__(self, **kwargs): + pass + async def __aenter__(self): + return self + async def __aexit__(self, *args): + pass + def get(self, url, params): + return Response([]) + def post(self, url, json): + created.append(json) + return Response({"id": "new-cloud-agent"}) + monkeypatch.setattr(bridge, "ClientSession", Client) + assert await bridge._cloud_agent({"LETTA_API_KEY": "key", "LETTA_BASE_URL": "https://cloud.example"}, + "ctx", "gateway/model") == "new-cloud-agent" + assert created[0]["model"] == created[0]["compaction_settings"]["model"] == "gateway/model" + assert "git-memory-enabled" in created[0]["tags"] + assert not created[0]["include_base_tools"] # tools execute in the CLI's task sandbox + + +@pytest.mark.asyncio +async def test_restored_context_cannot_silently_switch_backends(monkeypatch, tmp_path): + monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path)) + home = bridge.context_data_dir("ctx") + home.mkdir() + (home / "agent-id.json").write_text('{"id":"cloud-agent","backend":"cloud","base_url":"https://cloud.example"}') + @asynccontextmanager + async def provider(*args): + yield "provider/model" + monkeypatch.setattr(bridge, "_provider", provider) + with pytest.raises(ValueError, match="backend"): + await bridge.run_letta_code("hello", "ctx") + with pytest.raises(ValueError, match="base URL"): + await bridge.run_letta_code("hello", "ctx", letta_api_key="key", letta_base_url="https://other.example") + + +@pytest.mark.asyncio +async def test_deployment_delivers_secret_backed_cloud_config_via_negotiated_extension(monkeypatch): + from types import SimpleNamespace + + from agent_env.a2a_agent import A2AAgent + from agent_env.task_step.context import TaskStepContext + from agent_env.task_step.task_steps import deploy_agent + from tst.util.config import config_with_document + + config = config_with_document({"agents": {"config": {"solver": { + "letta_api_key": "secret:cloud", "letta_base_url": "https://cloud.example", + }}}}) + monkeypatch.setattr(config, "_resolve_secret", lambda key: "resolved-cloud-key") + monkeypatch.setattr(deploy_agent, "get_config", lambda: config) + card = a2a_server.build_app(a2a_server.LettaCodeAgent()).state.agentenv_a2a.card.model_dump( + mode="json", by_alias=True, + ) + class Agent: + metadata = {} + async def deploy(self, **kwargs): + return SimpleNamespace(a2a_url="https://agent.example", agent_card=card, sandbox_id="sb", + sandbox_type="fake", instance_id="instance", network_policy=None) + async def close(self): + pytest.fail("A successful configured deployment should stay running") + monkeypatch.setattr(A2AAgent, "get", classmethod(lambda cls, *a, **kw: Agent())) + delivered = [] + async def post(url, fields): + delivered.append((url, fields)) + monkeypatch.setattr(deploy_agent.protocol, "post_agent_config", post) + await deploy_agent.DeployAgentTaskStep(id="deploy", version=None, a2a_agent_id="letta-code", + agent_name="solver").execute(TaskStepContext()) + assert ("https://agent.example/ext/agent-config", { + "letta_api_key": "resolved-cloud-key", "letta_base_url": "https://cloud.example", + }) in delivered From c17bd3b4aaffb72fdbbaf41635e460f4d3e5bc45 Mon Sep 17 00:00:00 2001 From: Letta Integration <300689746+letta-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:06:59 +0000 Subject: [PATCH 07/13] revert(agents): drop the optional Letta Cloud backend for now --- .agentenv/config.example.toml | 4 - src/agent_env/agents/README.md | 7 +- .../agents/agents/letta-code/README.md | 63 +--- .../agents/agents/letta-code/a2a_server.py | 5 - .../agents/letta-code/letta_code_bridge.py | 141 ++------- src/agent_env/config/describe.py | 2 +- src/agent_env/config/runtime.py | 13 +- .../task_step/task_steps/deploy_agent.py | 7 - tst/unit/agents/letta_code/test_cloud.py | 287 ------------------ 9 files changed, 29 insertions(+), 500 deletions(-) delete mode 100644 tst/unit/agents/letta_code/test_cloud.py diff --git a/.agentenv/config.example.toml b/.agentenv/config.example.toml index 41a741e6c..6146dee22 100644 --- a/.agentenv/config.example.toml +++ b/.agentenv/config.example.toml @@ -38,10 +38,6 @@ agent_default = "local" # agents too # [agents] # default_a2a_agent_id = "my-agent" # what a deploy_agent step that names no agent deploys; by default the # # agents bundle's Claude Code, @local/agentenv-framework/agents/claude-code -# Per-deployed-agent agent-config fields (negotiated with its Agent Card): -# [agents.config.default-agent] -# letta_api_key = "secret:LETTA_API_KEY" # optional cloud mode for letta-code -# letta_base_url = "https://api.letta.com" # [model] # base_url = "http://localhost:4000" # a model proxy on this machine; local containers reach it as host.docker.internal diff --git a/src/agent_env/agents/README.md b/src/agent_env/agents/README.md index f9a37b744..720aa4bf0 100644 --- a/src/agent_env/agents/README.md +++ b/src/agent_env/agents/README.md @@ -62,11 +62,10 @@ OpenAI-compatible one, `/v1/chat/completions`, and takes its model from th with glibc. Its folder's `README.md` covers the extensions it implements. `agents/letta-code/` runs [Letta Code](https://github.com/letta-ai/letta-code) behind the protocol's A2A agent -framework: each turn runs the CLI in headless mode, resumed in the context's conversation. Local mode keeps separate agent state per context; an optional secret-backed agent-config key -selects Letta Cloud with persistent server-side memory. Name it as `letta-code`, its alias, or by its id, +framework: each turn is one `letta --backend local -p --output-format stream-json` run, resumed in the context's +conversation, with a separate local agent and memory per context. Name it as `letta-code`, its alias, or by its id, `@local/agentenv-framework/agents/letta-code`. It calls the `[model]` endpoint as an OpenAI-compatible one through -a local relay that adds task attribution, or a cloud BYOK provider with the header limitations documented in -its README, and takes its model from the prompt, else `openai/gpt-4.1-mini`. It declares +a relay that adds task attribution, and takes its model from the prompt, else `openai/gpt-4.1-mini`. It declares `install/v1` too, into a container or onto a Linux host with glibc. Its folder's `README.md` covers the extensions it implements. Run its smoke task with `agent-env run agents --task letta-code-smoke`. diff --git a/src/agent_env/agents/agents/letta-code/README.md b/src/agent_env/agents/agents/letta-code/README.md index ec04bcd3e..9017d070b 100644 --- a/src/agent_env/agents/agents/letta-code/README.md +++ b/src/agent_env/agents/agents/letta-code/README.md @@ -5,12 +5,12 @@ in the `agents` bundle as `@local/agentenv-framework/agents/letta-code`, with al is the whole build context. The Dockerfile pins the Python base by digest and Node by version and checksum; `package-lock.json` pins `@letta-ai/letta-code` to 0.34.25. -By default each turn runs `letta --backend local -p --output-format stream-json --yolo`. The bridge creates a local agent with +Each turn runs `letta --backend local -p --output-format stream-json --yolo`. The bridge creates an agent with `letta agents create`; the first turn uses `--agent --new`, and subsequent turns use `--conversation `. Each context has a separate home under `$HOME/sessions/`. Different contexts run concurrently; turns in one context and snapshot operations are synchronized. -In local mode, the bridge connects the backend with `letta connect openai-compatible`. A per-turn loopback relay forwards +The bridge connects the local backend with `letta connect openai-compatible`. A per-turn loopback relay forwards requests to `$LITELLM_BASE_URL/v1`, with `LITELLM_API_KEY`, `[model.headers]` (`LITELLM_EXTRA_HEADERS`), and `project_id` / `task_id` as `x-litellm-customer-id` and `x-litellm-tags`. The model defaults to `openai/gpt-4.1-mini`. Endpoint credentials stay at the relay. Registered MCP servers retain their names and headers; @@ -20,13 +20,13 @@ direct Letta Code tools through `letta.tools.register`; each tool executes `lett | Extension | Implementation | |---|---| -| `agent-config/v1` | SDK set/get; `model`, `effort`, `system_prompt`, `output_format`, `project_id`, `task_id`, `timeout_seconds`, write-only `letta_api_key`, and `letta_base_url`. Local task instructions are committed to core memory; cloud task instructions are prepended to each turn. JSON schema answers are validated locally. | +| `agent-config/v1` | SDK set/get; `model`, `effort`, `system_prompt`, `output_format`, `project_id`, `task_id`, `timeout_seconds`. Before every turn, task instructions are committed to the existing agent's core memory, which the local backend compiles into its system prompt. JSON schema answers are validated locally. | | `mcp-config/v1` | SDK registration and listing; servers passed to the context's agent settings each turn, with their discovered schemas exposed as direct tools through the public mods API. | | `skill-config/v1` | Inline and bundle skills installed under `$HOME/.letta/skills/`, copied into each context's home before its turn. | | `trajectory/v1` | Native stream-json events appended live, format `letta-code-events/1`. | -| `snapshot/v1` | Local save/load archives and restores the context home, conversations and git-backed memory, rewriting home paths. Cloud save/load records and rebinds the agent ID, conversation ID and base URL; it references live server memory rather than copying or rolling it back. Both support optional workspace archives. GNU tar changelog captures completed-turn file increments, including removed roots. | +| `snapshot/v1` | Save archives the complete context home, including conversations, agent records, settings and git-backed memory; load stages it and restores it into the target context, rewriting stored home paths and binding the saved conversation. Optional workspace archives restore `/app`. The validator reports load/recall as false without its optional Claude Code rubric judge, as it does for the OpenCode baseline; a live cross-context recall probe verifies restore independently. GNU tar changelog captures task-level file increments, including removed roots, using the OpenCode template's implementation. | | `peer-agents/v1` | SDK default; its `peer_agents` MCP server is registered alongside task servers. | -| `attribution-probe/v1` | Reports attribution forwarded by the local relay. Cloud provider calls cannot carry per-turn attribution; they do not update this probe. | +| `attribution-probe/v1` | Reports the project and task attribution used by the last CLI turn. | | `install/v1` | Template installer brings checksum-verified Node and uv, the pinned CLI, and a private Python into a container or a Linux host with glibc. Installs git if missing. | | `staging/v1` | SDK default. | @@ -37,56 +37,3 @@ The end of CLI stderr is included in failures. Cancellation and shutdown kill th Run `agent-env run agents --task letta-code-smoke` for the shell smoke task and `agent-env a2a-agent validate --id letta-code --sandbox local` for the repository's validator. - -## Backends - -Local mode is the default when `letta_api_key` is absent or empty. Configure cloud mode in -`.agentenv/config.toml`, using the **deployed agent name** (`default-agent` in the shipped smoke task): - -```toml -[agents.config.default-agent] -letta_api_key = "secret:LETTA_API_KEY" -letta_base_url = "https://api.letta.com" # optional, this is the default - -[model] -base_url = "https://your-public-gateway.example" -api_key = "secret:MODEL_API_KEY" -default = "openai/gpt-4.1-mini" -``` - -`[agents.config.]` supplies negotiated `agent-config/v1` fields at deployment. -`secret:` references resolve through `[stores.secret]`; `env:` references also work. The default -secret store reads the named environment variable, but secrets never belong literally in TOML. -The key is write-only and omitted from agent-config reads. This uses the repository's config file -and secret store instead of introducing a new environment switch. `agent.toml` remains optional; -putting a key in `[default_env_vars]` there would persist credentials with the agent definition. -The bridge sets the CLI's existing authentication variables internally and ignores inherited -`LETTA_*` settings so unrelated CLI sessions cannot select this run's backend or identity. - -With a key, the bridge omits `--backend local` and uses the CLI's default cloud backend. It -registers an OpenAI-compatible BYOK provider using the same `/v1/providers` API as the pinned -CLI's `connect` implementation (`provider_type = "openai"`). The provider points directly to -`$LITELLM_BASE_URL/v1` and stores the task endpoint's key on Letta Cloud. Both the agent model and -compaction model use `/`, retaining gateway routing. The endpoint must -be reachable from Letta Cloud; a sandbox loopback relay or private gateway cannot serve it. -A deterministic provider name hashes the context, endpoint and key, so sequential runs reuse it; -key rotation creates a new provider. Providers remain on the account for reuse. - -**Header limitation:** the pinned provider API supports a base URL and API key, but no arbitrary -model-call headers. Cloud mode therefore rejects nonempty `[model.headers]` instead of silently -dropping required headers. It cannot forward per-turn `x-litellm-customer-id` / `x-litellm-tags`, -so Scale gateway routing still applies but project/task header attribution does not. Use local -mode when those headers are required. The attribution probe does not claim cloud forwarding. - -An agent's stable name is `agentenv-` plus the first 32 hex characters of SHA-256 of the A2A -context ID. Cloud mode looks it up before creating it, preserving its server-side memory across -runs that use the same context. A new context starts a new agent; snapshot load can explicitly -bind another context to the saved agent and conversation. Without a saved conversation ID, -a run starts a new conversation on the reused agent. Keep independent concurrent runs on -separate contexts; cross-process creation of the same context is not serialized. - -Cloud snapshots contain only IDs and the base URL, never the cloud key or server memory. -Restore requires a configured key with access to the original agent on the same base URL. -It resumes the saved conversation against the agent's **current** memory: no offline restore, -point-in-time rollback, account migration or cloning is provided. Workspace save/load remains -available. Local snapshots retain their full state-copy behavior. diff --git a/src/agent_env/agents/agents/letta-code/a2a_server.py b/src/agent_env/agents/agents/letta-code/a2a_server.py index 622db785f..eb214936a 100644 --- a/src/agent_env/agents/agents/letta-code/a2a_server.py +++ b/src/agent_env/agents/agents/letta-code/a2a_server.py @@ -54,7 +54,6 @@ TaskResult, TextPart, Usage, - WriteOnly, a2a_agent, download, enable, @@ -131,8 +130,6 @@ def _changelog_backup_exclusions() -> list[str]: class LettaCodeConfig(AgentConfig): - letta_api_key: WriteOnly[str | None] = None - letta_base_url: str = "https://api.letta.com" model: str | None = None effort: str | None = None system_prompt: str | None = None @@ -345,8 +342,6 @@ def record(event: dict) -> None: context_id=context_id, session_id=session_id, model=request.config.model, - letta_api_key=request.config.letta_api_key, - letta_base_url=request.config.letta_base_url, effort=request.config.effort, mcp_servers=dict(request.mcp_servers), system_prompt=request.config.system_prompt, diff --git a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py index b6b546056..0059ce5ec 100644 --- a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py +++ b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py @@ -1,4 +1,4 @@ -"""Letta Code bridge with local context state or persistent cloud agents.""" +"""Letta Code local-backend bridge, with isolated context state and a per-turn model relay.""" from __future__ import annotations import asyncio @@ -52,17 +52,11 @@ def context_data_dir(context_id: str) -> Path: return Path(SESSIONS_ROOT) / ctx_hash -def _context_env(context_id: str, api_key: str | None = None, base_url: str = "https://api.letta.com") -> dict[str, str]: +def _context_env(context_id: str) -> dict[str, str]: home = context_data_dir(context_id) home.mkdir(parents=True, exist_ok=True) - environ = {key: value for key, value in os.environ.items() - if not key.startswith("LETTA_")} - if api_key: - environ.update(LETTA_API_KEY=api_key, LETTA_BASE_URL=base_url, LETTA_BACKEND="api") - else: - environ["LETTA_BACKEND"] = "local" - return {**environ, "HOME": str(home), "LETTA_LOCAL_BACKEND_DIR": str(home / ".letta/lc-local-backend"), - "LETTA_LOCAL_BACKEND_EXPERIMENTAL": "0" if api_key else "1"} + return {**os.environ, "HOME": str(home), "LETTA_LOCAL_BACKEND_DIR": str(home / ".letta/lc-local-backend"), + "LETTA_LOCAL_BACKEND_EXPERIMENTAL": "1"} async def _communicate(*cmd: str, env: Mapping[str, str], timeout_s: int) -> tuple[int, bytes, bytes]: @@ -89,13 +83,7 @@ async def export_session(context_id: str, session_id: str) -> bytes: (root / "snapshot-session.json").write_text(json.dumps({"session_id": session_id})) buffer = io.BytesIO() with tarfile.open(fileobj=buffer, mode="w:gz") as archive: - agent_file = root / "agent-id.json" - cloud = agent_file.exists() and json.loads(agent_file.read_text()).get("backend") == "cloud" - if cloud: - for name in ("agent-id.json", "context.json", "snapshot-session.json"): - archive.add(root / name, arcname="state/" + name) - else: - archive.add(root, arcname="state") + archive.add(root, arcname="state") return buffer.getvalue() @@ -317,82 +305,13 @@ async def forward(request): await runner.cleanup() -def _cli(env: Mapping[str, str]) -> list[str]: - return ["letta"] if env.get("LETTA_API_KEY") else ["letta", "--backend", "local"] - - -@asynccontextmanager -async def _provider(env, context_id, model, attribution): - if not env.get("LETTA_API_KEY"): - async with model_relay(provider_headers(os.environ, attribution)) as relay: - await _command(env, "connect", "openai-compatible", "--base-url", relay, "--api-key", "relay") - yield "openai-compatible/" + model - return - # The cloud provider API used by CLI 0.34.25 carries a URL and key, but no arbitrary headers. - # Never silently discard endpoint headers (which may themselves be authentication). - if model_headers(os.environ.get(MODEL_HEADERS_ENV)): - raise ValueError("Letta Cloud BYOK cannot forward [model.headers]; use local mode for this endpoint") - base = _base_url(os.environ) - if not os.environ.get("LITELLM_BASE_URL"): - raise ValueError("Cloud mode requires the task's [model] endpoint") - # A provider belongs to a context and endpoint/key combination. Credential rotation gets a new - # provider without changing another context's in-flight calls. No raw credential is persisted locally. - identity = json.dumps([context_id, base, os.environ.get("LITELLM_API_KEY", "")]) - name = "agentenv-" + hashlib.sha256(identity.encode()).hexdigest()[:32] - async with ClientSession(headers={"Authorization": "Bearer " + env["LETTA_API_KEY"]}, - timeout=ClientTimeout(total=_CLI_TIMEOUT_S)) as client: - url = env["LETTA_BASE_URL"].rstrip("/") + "/v1/providers" - async with client.get(url) as response: - response.raise_for_status() - providers = await response.json() - if not any(provider["name"] == name for provider in providers): - async with client.post(url, json={"name": name, "provider_type": "openai", "base_url": base, - "api_key": os.environ.get("LITELLM_API_KEY", "")}) as response: - # A second process may have created the same deterministic provider. - if response.status == 409: - async with client.get(url) as retry: - retry.raise_for_status() - if not any(item["name"] == name for item in await retry.json()): - response.raise_for_status() - else: - response.raise_for_status() - yield name + "/" + model - - async def _command(env: dict[str, str], *args: str) -> bytes: - code, out, err = await _communicate(*_cli(env), *args, env=env, timeout_s=120) + code, out, err = await _communicate("letta", "--backend", "local", *args, env=env, timeout_s=120) if code: raise RuntimeError(f"Letta Code {args[0]} failed: {err.decode(errors='replace')[-ERROR_MESSAGE_LIMIT:]}") return out -async def _cloud_agent(env, context_id, handle, agent_id=None): - """Resolve the context on the server and bind both inference and compaction to BYOK.""" - base = env["LETTA_BASE_URL"].rstrip("/") + "/v1/agents" - name = "agentenv-" + hashlib.sha256(context_id.encode()).hexdigest()[:32] - async with ClientSession(headers={"Authorization": "Bearer " + env["LETTA_API_KEY"]}, - timeout=ClientTimeout(total=_CLI_TIMEOUT_S)) as client: - if not agent_id: - async with client.get(base, params={"name": name}) as response: - response.raise_for_status() - agents = await response.json() - if agents: - agent_id = agents[0]["id"] - config = {"model": handle, "compaction_settings": {"model": handle}} - if agent_id: - async with client.patch(base + "/" + agent_id, json=config) as response: - response.raise_for_status() - else: - async with client.post(base, json={ - **config, "name": name, "tags": ["origin:letta-code", "git-memory-enabled"], - "include_base_tools": False, "include_base_tool_rules": False, - "tools": [], "initial_message_sequence": [], - }) as response: - response.raise_for_status() - agent_id = (await response.json())["id"] - return agent_id - - def _configure_agent(home: Path, agent_id: str, servers: dict, role: str | None, system_prompt: str | None): # Core memory's system/ files are compiled into the existing agent's system prompt on every run. memory = home / ".letta/lc-local-backend/memfs" / agent_id / "memory" @@ -412,16 +331,12 @@ def _configure_agent(home: Path, agent_id: str, servers: dict, role: str | None, "-m", "Update task instructions", "--", target]): subprocess.run(["git", *args], cwd=memory, check=True, capture_output=True) - _configure_settings(home, agent_id, servers, role) - - -def _configure_settings(home, agent_id, servers, role, base_url=None): settings_path = home / ".letta/settings.json" settings = json.loads(settings_path.read_text()) if settings_path.exists() else {} agents = settings.setdefault("agents", []) matching = [agent for agent in agents if agent.get("agentId") == agent_id] if not matching: - matching = [{"agentId": agent_id, "baseUrl": base_url or "local:" + str(home / ".letta/lc-local-backend")}] + matching = [{"agentId": agent_id, "baseUrl": "local:" + str(home / ".letta/lc-local-backend")}] agents.extend(matching) for agent in matching: agent["mcpServers"] = [ @@ -445,52 +360,36 @@ async def run_letta_code( mcp_servers: dict[str, dict] | None = None, system_prompt: str | None = None, output_format: dict | None = None, timeout_s: int = 600, project_id: str | None = None, task_id: str | None = None, role: str | None = None, attachment_paths: list[str] | None = None, - letta_api_key: str | None = None, letta_base_url: str = "https://api.letta.com", effort: str | None = None, event_callback: Callable[[dict], None] | None = None, workspace: str = "/app", ) -> LettaCodeResult: """A bounded CLI turn, including provider setup; cancellation kills the process tree.""" try: async with asyncio.timeout(timeout_s): return await _turn(prompt, context_id, session_id, model, mcp_servers, system_prompt, output_format, - project_id, task_id, role, attachment_paths, effort, event_callback, workspace, - letta_api_key, letta_base_url) + project_id, task_id, role, attachment_paths, effort, event_callback, workspace) except TimeoutError: return LettaCodeResult(response="", tool_call_count=0, session_id=session_id, error=f"Letta Code timed out after {timeout_s}s") async def _turn(prompt, context_id, session_id, model, mcp_servers, system_prompt, output_format, - project_id, task_id, role, attachment_paths, effort, event_callback, workspace, - letta_api_key=None, letta_base_url="https://api.letta.com"): + project_id, task_id, role, attachment_paths, effort, event_callback, workspace): model = model or DEFAULT_MODEL - env = _context_env(context_id, letta_api_key, letta_base_url) + handle = "openai-compatible/" + model + env = _context_env(context_id) home = Path(env["HOME"]) (home / "context.json").write_text(json.dumps({"home": str(home)})) attribution = _attribution_headers(project_id, task_id) - async with _provider(env, context_id, model, attribution) as handle: + async with model_relay(provider_headers(os.environ, attribution)) as relay: + await _command(env, "connect", "openai-compatible", "--base-url", relay, "--api-key", "relay") agent_file = home / "agent-id.json" if agent_file.exists(): - saved = json.loads(agent_file.read_text()) - if (saved.get("backend", "local") == "cloud") != bool(letta_api_key): - raise ValueError("Context backend differs from the configured backend") - if letta_api_key and saved.get("base_url") != letta_base_url: - raise ValueError("Cloud snapshot belongs to another Letta base URL") - agent_id = saved["id"] + agent_id = json.loads(agent_file.read_text())["id"] else: - agent_id = None - if letta_api_key: - agent_id = await _cloud_agent(env, context_id, handle, agent_id) - elif agent_id is None: agent = json.loads(await _command(env, "agents", "create", "--model", handle)) agent_id = agent["id"] - agent_file.write_text(json.dumps({"id": agent_id, "backend": "cloud" if letta_api_key else "local", - "base_url": letta_base_url if letta_api_key else None})) - if letta_api_key: - _configure_settings(home, agent_id, mcp_servers or {}, role, letta_base_url) - if system_prompt: - prompt = system_prompt + "\n\n" + prompt - else: - _configure_agent(home, agent_id, mcp_servers or {}, role, system_prompt) + agent_file.write_text(json.dumps({"id": agent_id})) + _configure_agent(home, agent_id, mcp_servers or {}, role, system_prompt) tools = json.loads(await _command(env, "mcp", "tools", "--full", "--agent", agent_id)) if mcp_servers else [] _install_mcp_tools(home, agent_id, tools) skills = Path(os.environ.get("HOME", "/data")) / ".letta/skills" @@ -503,7 +402,7 @@ async def _turn(prompt, context_id, session_id, model, mcp_servers, system_promp prompt += "\nReturn only JSON matching this schema: " + json.dumps(schema) for path in attachment_paths or []: prompt += "\nRead the attached file at " + path - cmd = [*_cli(env), "-p", prompt, "--output-format", "stream-json", "--yolo", + cmd = ["letta", "--backend", "local", "-p", prompt, "--output-format", "stream-json", "--yolo", "--model", handle, "--toolset", "default"] cmd += ["--conversation", session_id] if session_id else ["--agent", agent_id, "--new"] if effort: @@ -515,11 +414,7 @@ async def _turn(prompt, context_id, session_id, model, mcp_servers, system_promp )) _RUNNING.add(proc) parser = _EventParser(event_callback) - if not letta_api_key: - _record_attribution(project_id, task_id) - else: - with _attribution_lock: - _last_attribution.update(project_id=None, task_id=None, last_seen_at_utc=None) + _record_attribution(project_id, task_id) try: stderr = await _stream_cli(proc, parser.on_line) except BaseException: diff --git a/src/agent_env/config/describe.py b/src/agent_env/config/describe.py index 35eb44cf8..1445a0b3c 100644 --- a/src/agent_env/config/describe.py +++ b/src/agent_env/config/describe.py @@ -276,7 +276,7 @@ class FileSection: _FILE_SECTIONS: tuple[FileSection, ...] = ( FileSection("model", ("model",), "config.runtime", dict.fromkeys(f.name for f in fields(ModelConfig))), FileSection("conversations", ("conversations",), "config.runtime", _resolved_keys("conversations")), - FileSection("agents", ("agents",), "config.runtime", {**_resolved_keys("agents"), "config": Named(None)}, validated_agents_section), + FileSection("agents", ("agents",), "config.runtime", _resolved_keys("agents"), validated_agents_section), FileSection("sandbox", ("sandbox",), "providers.sandbox_providers.sandbox_provider", {"default": None, "agent_default": None, "attribution": None, "providers": Named(_SEAM)}), FileSection("state", ("state",), "providers.env_state.env_state_provider", {"providers": Named(_SEAM)}), diff --git a/src/agent_env/config/runtime.py b/src/agent_env/config/runtime.py index a7a9972e1..952d931df 100644 --- a/src/agent_env/config/runtime.py +++ b/src/agent_env/config/runtime.py @@ -96,16 +96,12 @@ def _validated_a2a_agent_id(value: object, source: str) -> str: def validated_agents_section(section: Mapping[str, Any]) -> Mapping[str, Any]: """The ``[agents]`` table, checked as the reader checks it; ``config show`` runs this too.""" - unknown = set(section) - {_AGENTS_CONFIG_KEY, "config"} + unknown = set(section) - {_AGENTS_CONFIG_KEY} if unknown: raise ConfigError( f"[{_AGENTS_CONFIG_SECTION}] has unknown keys {sorted(unknown)}; " - f"allowed: ['{_AGENTS_CONFIG_KEY}', 'config']" + f"allowed: ['{_AGENTS_CONFIG_KEY}']" ) - if not isinstance(section.get("config", {}), dict): - raise ConfigError("[agents.config] must be a table keyed by deployed agent name") - if any(not isinstance(fields, dict) for fields in section.get("config", {}).values()): - raise ConfigError("Each [agents.config.] must be a table of agent-config fields") if _AGENTS_CONFIG_KEY in section: _validated_a2a_agent_id( section[_AGENTS_CONFIG_KEY], f"[{_AGENTS_CONFIG_SECTION}] {_AGENTS_CONFIG_KEY}" @@ -388,11 +384,6 @@ def get_default_human_a2a_url(self) -> str: return _validated_human_a2a_url(resolved, key.file_label) raise ConfigError(f"No human-A2A URL configured: set [{_HUMAN_A2A_CONFIG_SECTION}] {_HUMAN_A2A_CONFIG_KEY} or AGENT_ENV_HUMAN_A2A_URL") - def get_agent_config(self, agent_name: str) -> dict[str, Any]: - """Negotiated extension fields for a deployed name, with secret references resolved.""" - section = validated_agents_section(self.section(_AGENTS_CONFIG_SECTION)) - return self.resolve_reference(section.get("config", {}).get(agent_name, {})) - def get_default_a2a_agent_id(self) -> str: """The agent a ``deploy_agent`` step without an id deploys: ``configure(default_a2a_agent_id=...)`` > ``[agents] default_a2a_agent_id`` > the agents bundle's Claude Code.""" diff --git a/src/agent_env/task_step/task_steps/deploy_agent.py b/src/agent_env/task_step/task_steps/deploy_agent.py index 8290ceed3..f2b3728b6 100644 --- a/src/agent_env/task_step/task_steps/deploy_agent.py +++ b/src/agent_env/task_step/task_steps/deploy_agent.py @@ -479,13 +479,6 @@ async def _execute(self, context: TaskStepContext, deployed_agents: list) -> Tas card.update(identity.fields) logger.info(f"Stamped card identity on '{self.agent_name}': {sorted(identity.fields)}") - # Per-agent extension config uses the same secret references as model credentials. - configured = config.get_agent_config(self.agent_name) - if configured: - negotiated = A2AAgent.negotiate_agent_config(card, configured) - if negotiated: - await protocol.post_agent_config(a2a_url + negotiated.endpoint, negotiated.fields) - default_model = config.get_model_for_role("agent") if default_model and not context.default_agent_model: context.default_agent_model = default_model diff --git a/tst/unit/agents/letta_code/test_cloud.py b/tst/unit/agents/letta_code/test_cloud.py deleted file mode 100644 index ef7152aec..000000000 --- a/tst/unit/agents/letta_code/test_cloud.py +++ /dev/null @@ -1,287 +0,0 @@ -"""Cloud selection, provider reuse, CLI execution and reference-only snapshots.""" -import json -from contextlib import asynccontextmanager - -import pytest - -from tst.unit.agents.letta_code.agent import a2a_server, letta_code_bridge as bridge - - -@pytest.mark.parametrize("key", [None, "cloud-secret"]) -def test_backend_is_selected_by_agent_config_not_inherited_credentials(monkeypatch, tmp_path, key): - monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path)) - monkeypatch.setenv("LETTA_API_KEY", "unrelated-shell-key") - monkeypatch.setenv("LETTA_BASE_URL", "https://unrelated.example") - monkeypatch.setenv("LETTA_CODE_AGENT_ROLE", "subagent") - monkeypatch.setenv("LETTA_LOCAL_BACKEND_EXPERIMENTAL", "1") - env = bridge._context_env("ctx", key, "https://cloud.example") - assert "LETTA_CODE_AGENT_ROLE" not in env - assert env["LETTA_LOCAL_BACKEND_EXPERIMENTAL"] == ("0" if key else "1") - assert env["LETTA_BACKEND"] == ("api" if key else "local") - if key: - assert env["LETTA_API_KEY"] == key and env["LETTA_BASE_URL"] == "https://cloud.example" - assert bridge._cli(env) == ["letta"] - else: - assert "LETTA_API_KEY" not in env and "LETTA_BASE_URL" not in env - assert bridge._cli(env) == ["letta", "--backend", "local"] - - -def test_key_is_write_only_in_agent_config(): - schema = a2a_server.LettaCodeConfig.model_json_schema() - assert schema["properties"]["letta_api_key"]["writeOnly"] - assert a2a_server.LettaCodeConfig().letta_base_url == "https://api.letta.com" - - -@pytest.mark.asyncio -async def test_cloud_provider_registers_gateway_once_and_reuses_it(monkeypatch): - monkeypatch.setenv("LITELLM_BASE_URL", "https://gateway.example") - monkeypatch.setenv("LITELLM_API_KEY", "gateway-key") - monkeypatch.delenv(bridge.MODEL_HEADERS_ENV, raising=False) - providers, writes = [], [] - - class Response: - status = 200 - async def __aenter__(self): - return self - async def __aexit__(self, *args): - pass - def raise_for_status(self): - pass - async def json(self): - return providers - - class Client: - def __init__(self, **kwargs): - assert kwargs["headers"] == {"Authorization": "Bearer cloud-key"} - async def __aenter__(self): - return self - async def __aexit__(self, *args): - pass - def get(self, url): - assert url == "https://cloud.example/v1/providers" - return Response() - def post(self, url, json): - writes.append(json) - providers.append({"name": json["name"]}) - return Response() - - monkeypatch.setattr(bridge, "ClientSession", Client) - env = {"LETTA_API_KEY": "cloud-key", "LETTA_BASE_URL": "https://cloud.example"} - handles = [] - for _ in range(2): - async with bridge._provider(env, "ctx", "openai/gpt-4.1-mini", {}) as handle: - handles.append(handle) - assert handles[0] == handles[1] == writes[0]["name"] + "/openai/gpt-4.1-mini" - assert len(writes) == 1 - assert writes[0]["base_url"] == "https://gateway.example/v1" - assert writes[0]["api_key"] == "gateway-key" and writes[0]["provider_type"] == "openai" - monkeypatch.setenv(bridge.MODEL_HEADERS_ENV, '{"X-Required": "secret"}') - with pytest.raises(ValueError, match=r"\[model.headers\]"): - async with bridge._provider(env, "ctx", "model", {}): - pytest.fail("Must not discard required endpoint headers") - - -@pytest.mark.asyncio -@pytest.mark.parametrize("conversation", [None, "conv-existing"]) -async def test_cloud_turn_resumes_agent_and_builds_cloud_process(monkeypatch, tmp_path, conversation): - monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path)) - commands, processes = [], [] - - @asynccontextmanager - async def provider(env, context, model, attribution): - assert env["LETTA_API_KEY"] == "cloud-key" - assert model == "task-model" - yield "run-provider/task-model" - - async def command(env, *args): - commands.append(args) - assert env["LETTA_BASE_URL"] == "https://cloud.example" - return b'{"items":[{"id":"agent-existing"}]}' if args[:2] == ("agents", "list") else b'{}' - - class Proc: - returncode = 0 - async def spawn(*args, **kwargs): - processes.append((args, kwargs)) - return Proc() - async def stream(proc, on_line): - on_line('{"type":"system","conversation_id":"conv-result"}') - on_line('{"type":"result","subtype":"success","result":"42"}') - return b'' - - async def cloud_agent(env, context, handle, agent_id): - assert context == "ctx" and handle == "run-provider/task-model" - return "agent-existing" - - monkeypatch.setattr(bridge, "_cloud_agent", cloud_agent) - monkeypatch.setattr(bridge, "_provider", provider) - monkeypatch.setattr(bridge, "_command", command) - monkeypatch.setattr(bridge.asyncio, "create_subprocess_exec", spawn) - monkeypatch.setattr(bridge, "_stream_cli", stream) - result = await bridge.run_letta_code("answer", "ctx", model="task-model", session_id=conversation, - letta_api_key="cloud-key", letta_base_url="https://cloud.example", - system_prompt="Use the task rules", effort="low", workspace=str(tmp_path)) - assert result.response == "42" and result.session_id == "conv-result" and result.error is None - args, kwargs = processes[0] - assert "--backend" not in args - assert args[args.index("--model") + 1] == "run-provider/task-model" - assert "Use the task rules" in args[args.index("-p") + 1] - assert kwargs["env"]["LETTA_AGENT_ID"] == "agent-existing" - assert "--conversation" in args if conversation else "--new" in args - assert not any(command[:2] == ("agents", "create") for command in commands) - assert any(command[:2] == ("model", "set") for command in commands) - # A fresh server can rebind IDs; credentials and endpoint headers are not in the archive. - home = bridge.context_data_dir("ctx") - (home / "secret-settings").write_text("do not export") - snapshot = tmp_path / "cloud.tgz" - snapshot.write_bytes(await bridge.export_session("ctx", result.session_id)) - assert await bridge.import_session("restored", str(snapshot)) == "conv-result" - restored = bridge.context_data_dir("restored") - assert json.loads((restored / "agent-id.json").read_text())["id"] == "agent-existing" - assert not (restored / "secret-settings").exists() - assert not (restored / ".letta/settings.json").exists() - - -def test_config_resolves_only_the_selected_agent_secrets(monkeypatch): - from agent_env.config.runtime import validated_agents_section - from tst.util.config import config_with_document - - config = config_with_document({"agents": {"config": { - "solver": {"letta_api_key": "secret:cloud", "letta_base_url": "https://cloud.example"}, - "other": {"letta_api_key": "secret:missing"}, - }}}) - requested = [] - def secret(key): - requested.append(key) - return "resolved-key" - monkeypatch.setattr(config, "_resolve_secret", secret) - assert config.get_agent_config("solver") == { - "letta_api_key": "resolved-key", "letta_base_url": "https://cloud.example", - } - assert requested == ["cloud"] - assert config.get_agent_config("absent") == {} - from agent_env.config.errors import ConfigError - with pytest.raises(ConfigError, match="table"): - validated_agents_section({"config": {"solver": "bad"}}) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("bound", [None, "agent-existing"]) -async def test_cloud_agent_reuses_context_and_routes_compaction_through_gateway(monkeypatch, bound): - writes = [] - class Response: - async def __aenter__(self): - return self - async def __aexit__(self, *args): - pass - def raise_for_status(self): - pass - async def json(self): - return [{"id": "agent-existing"}] - class Client: - def __init__(self, **kwargs): - pass - async def __aenter__(self): - return self - async def __aexit__(self, *args): - pass - def get(self, url, params): - assert params["name"].startswith("agentenv-") - return Response() - def patch(self, url, json): - writes.append((url, json)) - return Response() - def post(self, *args, **kwargs): - pytest.fail("Must reuse the existing cloud agent") - monkeypatch.setattr(bridge, "ClientSession", Client) - agent = await bridge._cloud_agent({"LETTA_API_KEY": "key", "LETTA_BASE_URL": "https://cloud.example"}, - "ctx", "provider/model", bound) - assert agent == "agent-existing" - assert writes == [("https://cloud.example/v1/agents/agent-existing", { - "model": "provider/model", "compaction_settings": {"model": "provider/model"}, - })] - - -@pytest.mark.asyncio -async def test_cloud_agent_creation_uses_task_model_and_persistent_memory(monkeypatch): - created = [] - class Response: - def __init__(self, data): - self.data = data - async def __aenter__(self): - return self - async def __aexit__(self, *args): - pass - def raise_for_status(self): - pass - async def json(self): - return self.data - class Client: - def __init__(self, **kwargs): - pass - async def __aenter__(self): - return self - async def __aexit__(self, *args): - pass - def get(self, url, params): - return Response([]) - def post(self, url, json): - created.append(json) - return Response({"id": "new-cloud-agent"}) - monkeypatch.setattr(bridge, "ClientSession", Client) - assert await bridge._cloud_agent({"LETTA_API_KEY": "key", "LETTA_BASE_URL": "https://cloud.example"}, - "ctx", "gateway/model") == "new-cloud-agent" - assert created[0]["model"] == created[0]["compaction_settings"]["model"] == "gateway/model" - assert "git-memory-enabled" in created[0]["tags"] - assert not created[0]["include_base_tools"] # tools execute in the CLI's task sandbox - - -@pytest.mark.asyncio -async def test_restored_context_cannot_silently_switch_backends(monkeypatch, tmp_path): - monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path)) - home = bridge.context_data_dir("ctx") - home.mkdir() - (home / "agent-id.json").write_text('{"id":"cloud-agent","backend":"cloud","base_url":"https://cloud.example"}') - @asynccontextmanager - async def provider(*args): - yield "provider/model" - monkeypatch.setattr(bridge, "_provider", provider) - with pytest.raises(ValueError, match="backend"): - await bridge.run_letta_code("hello", "ctx") - with pytest.raises(ValueError, match="base URL"): - await bridge.run_letta_code("hello", "ctx", letta_api_key="key", letta_base_url="https://other.example") - - -@pytest.mark.asyncio -async def test_deployment_delivers_secret_backed_cloud_config_via_negotiated_extension(monkeypatch): - from types import SimpleNamespace - - from agent_env.a2a_agent import A2AAgent - from agent_env.task_step.context import TaskStepContext - from agent_env.task_step.task_steps import deploy_agent - from tst.util.config import config_with_document - - config = config_with_document({"agents": {"config": {"solver": { - "letta_api_key": "secret:cloud", "letta_base_url": "https://cloud.example", - }}}}) - monkeypatch.setattr(config, "_resolve_secret", lambda key: "resolved-cloud-key") - monkeypatch.setattr(deploy_agent, "get_config", lambda: config) - card = a2a_server.build_app(a2a_server.LettaCodeAgent()).state.agentenv_a2a.card.model_dump( - mode="json", by_alias=True, - ) - class Agent: - metadata = {} - async def deploy(self, **kwargs): - return SimpleNamespace(a2a_url="https://agent.example", agent_card=card, sandbox_id="sb", - sandbox_type="fake", instance_id="instance", network_policy=None) - async def close(self): - pytest.fail("A successful configured deployment should stay running") - monkeypatch.setattr(A2AAgent, "get", classmethod(lambda cls, *a, **kw: Agent())) - delivered = [] - async def post(url, fields): - delivered.append((url, fields)) - monkeypatch.setattr(deploy_agent.protocol, "post_agent_config", post) - await deploy_agent.DeployAgentTaskStep(id="deploy", version=None, a2a_agent_id="letta-code", - agent_name="solver").execute(TaskStepContext()) - assert ("https://agent.example/ext/agent-config", { - "letta_api_key": "resolved-cloud-key", "letta_base_url": "https://cloud.example", - }) in delivered From 741da14256b8c50747d89c0bce897f836b5a7c9c Mon Sep 17 00:00:00 2001 From: Letta Integration <300689746+letta-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:27:21 +0000 Subject: [PATCH 08/13] fix(agents): make Letta Code snapshot restore transactional and cancellation-safe Stage the restored conversation before touching the target context, swap it in only after the workspace swap succeeds, and put /app back if it fails. Run snapshot file work in a thread that a cancelled caller still waits for, so cancellation can no longer release the lock or delete staging mid-move. Record attribution when the relay forwards a model request, not at CLI start. Name the CLI timeout and relay size limit, and pin Node 22.23.4, the minimum the locked CLI supports. --- .../agents/agents/letta-code/Dockerfile | 6 +- .../agents/agents/letta-code/a2a_server.py | 48 ++++++--- .../agents/letta-code/letta_code_bridge.py | 87 ++++++++++++---- tst/unit/agents/letta_code/test_bridge.py | 99 ++++++++++++++++++- 4 files changed, 202 insertions(+), 38 deletions(-) diff --git a/src/agent_env/agents/agents/letta-code/Dockerfile b/src/agent_env/agents/agents/letta-code/Dockerfile index 6ead4f69e..53cc4b5a8 100644 --- a/src/agent_env/agents/agents/letta-code/Dockerfile +++ b/src/agent_env/agents/agents/letta-code/Dockerfile @@ -1,9 +1,9 @@ FROM public.ecr.aws/docker/library/python:3.11-slim@sha256:c20888b6acdd1e63e1c433a185bf3ad162c0288fe484616ce062e0d28add2900 # Node to install the Letta Code CLI, a native binary, and for the model's commands, checked against its published # checksum; install.sh brings the same. poppler-utils renders a PDF attachment to the page images the model reads. -ARG NODE_VERSION=22.23.3 -ARG NODE_SHA256_X64=1084aa36196bba4c3a5e69a1ee388a6e4ff729dad09445fbcd434b28fe3c24af -ARG NODE_SHA256_ARM64=5ced2d48d1d7198739b7f86804de0171aefb6823b684b12341d3321afc3cb0b2 +ARG NODE_VERSION=22.23.4 +ARG NODE_SHA256_X64=b8492284c76a54baf58d61668dd0e41ee0fc98adabf9503d30f05d08f55b4406 +ARG NODE_SHA256_ARM64=f7a1e486dbc8b286847815b477d4fb5e9fb963ed4489c3e1588853d685f589f9 RUN set -eu; \ apt-get update && apt-get install -y --no-install-recommends ca-certificates curl poppler-utils git \ && rm -rf /var/lib/apt/lists/*; \ diff --git a/src/agent_env/agents/agents/letta-code/a2a_server.py b/src/agent_env/agents/agents/letta-code/a2a_server.py index eb214936a..d3bdd762b 100644 --- a/src/agent_env/agents/agents/letta-code/a2a_server.py +++ b/src/agent_env/agents/agents/letta-code/a2a_server.py @@ -10,6 +10,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import base64 import binascii import json @@ -66,11 +67,13 @@ from letta_code_bridge import ( ERROR_MESSAGE_LIMIT, + commit_staged_session, event_session_id, export_session, get_last_attribution_snapshot, - import_session, kill_running_clis, + shielded_thread, + stage_session, run_letta_code, validate_output_format, ) @@ -447,11 +450,23 @@ async def load_snapshot(self, request: ObjectSnapshotLoadRequest) -> dict[str, A check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, ) async with self._workspace.exclusive(), self._turn_lock(target_context_id): - imported_session_id = await import_session(target_context_id, str(session_file)) + # Stage the conversation first, so a bad snapshot changes nothing. The workspace swap and the + # conversation swap then run in one thread that cancellation can't abandon: a failed conversation + # swap puts /app back, and the context's previous conversation stays in place until both succeed. + session_dir = Path(directory) / "session" + session_dir.mkdir() + imported_session_id, staged_session = await shielded_thread( + stage_session, target_context_id, str(session_file), str(session_dir)) if not imported_session_id: raise HTTPException(status_code=500, detail="Letta Code import returned no session id") + + def commit_session() -> None: + commit_staged_session(target_context_id, staged_session) + if staged is not None: - await asyncio.to_thread(_replace_workspace, staged) + await shielded_thread(_replace_workspace, staged, commit_session) + else: + await shielded_thread(commit_session) self.set_session_ref_for_context(target_context_id, imported_session_id) return {"context_id": target_context_id} @@ -521,12 +536,25 @@ async def apply_changelog(self, request: ObjectChangelogApplyRequest) -> dict[st return {"count": len(request.increments), "context_id": request.target_context_id} -def _replace_workspace(staged: Path) -> None: +def _replace_workspace(staged: Path, then: Callable[[], None] | None = None) -> None: + """Swap staged in for /app's contents. When then() raises after the swap, /app's previous contents come back.""" WORKSPACE_DIR.mkdir(parents=True, exist_ok=True) backup = Path(tempfile.mkdtemp(prefix=".letta_code-backup-", dir=WORKSPACE_DIR)) saved = backup / "saved" saved.mkdir() committed = False + + def restore_saved() -> None: + for child in WORKSPACE_DIR.iterdir(): + if child == backup: + continue + if child.is_dir() and not child.is_symlink(): + shutil.rmtree(child) + else: + child.unlink() + for child in saved.iterdir(): + child.rename(WORKSPACE_DIR / child.name) + try: try: for child in WORKSPACE_DIR.iterdir(): @@ -539,17 +567,11 @@ def _replace_workspace(staged: Path) -> None: try: for child in staged.iterdir(): shutil.move(str(child), str(WORKSPACE_DIR / child.name)) + if then is not None: + then() committed = True except Exception: - for child in WORKSPACE_DIR.iterdir(): - if child == backup: - continue - if child.is_dir() and not child.is_symlink(): - shutil.rmtree(child) - else: - child.unlink() - for child in saved.iterdir(): - child.rename(WORKSPACE_DIR / child.name) + restore_saved() raise finally: if committed: diff --git a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py index 0059ce5ec..2a75941df 100644 --- a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py +++ b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py @@ -37,6 +37,8 @@ # under $HOME so host-mode instances don't share session state. SESSIONS_ROOT = os.path.join(os.environ.get("HOME", "/data"), "sessions") _CLI_TIMEOUT_S = 120 +# Largest model request body the relay accepts from the CLI. +_RELAY_MAX_REQUEST_BYTES = 100 * 1024**2 def _base_url(environ: Mapping[str, str]) -> str: @@ -77,8 +79,7 @@ async def _communicate(*cmd: str, env: Mapping[str, str], timeout_s: int) -> tup -async def export_session(context_id: str, session_id: str) -> bytes: - """Capture the conversation, agent, git-backed memory and local settings together.""" +def _archive_session(context_id: str, session_id: str) -> bytes: root = context_data_dir(context_id) (root / "snapshot-session.json").write_text(json.dumps({"session_id": session_id})) buffer = io.BytesIO() @@ -87,25 +88,67 @@ async def export_session(context_id: str, session_id: str) -> bytes: return buffer.getvalue() +async def export_session(context_id: str, session_id: str) -> bytes: + """Capture the conversation, agent, git-backed memory and local settings together, off the event loop.""" + return await shielded_thread(_archive_session, context_id, session_id) + + +async def shielded_thread(func, *args): + """Run blocking file work in a thread that cancellation can't abandon: a cancelled caller still waits for it.""" + task = asyncio.ensure_future(asyncio.to_thread(func, *args)) + try: + return await asyncio.shield(task) + except asyncio.CancelledError: + with contextlib.suppress(BaseException): + await task + raise + + +def stage_session(context_id: str, json_path: str, directory: str) -> tuple[str, Path]: + """Extract a snapshot's local state into directory, with paths rewritten for context_id; the target is untouched.""" + root = context_data_dir(context_id) + with tarfile.open(json_path, "r:gz") as archive: + archive.extractall(directory, filter="data") + staged = Path(directory) / "state" + session_id = json.loads((staged / "snapshot-session.json").read_text())["session_id"] + if not isinstance(session_id, str) or not session_id: + raise ValueError("Snapshot has no conversation id") + # Memory repos and settings may contain absolute paths to the original context. + source = json.loads((staged / "context.json").read_text())["home"] + for path in staged.rglob("*.json"): + if ".git" not in path.parts: + path.write_text(path.read_text().replace(source, str(root))) + return session_id, staged + + +def commit_staged_session(context_id: str, staged: Path) -> None: + """Swap a staged state tree in for the context's; the previous tree is restored if the move fails.""" + root = context_data_dir(context_id) + root.parent.mkdir(parents=True, exist_ok=True) + previous = root.with_name(root.name + ".previous") + if previous.exists(): + shutil.rmtree(previous) + if root.exists(): + root.rename(previous) + try: + shutil.move(str(staged), root) + except BaseException: + if root.exists(): + shutil.rmtree(root) + if previous.exists(): + previous.rename(root) + raise + if previous.exists(): + shutil.rmtree(previous) + + async def import_session(context_id: str, json_path: str) -> str: """Stage a complete local state tree, then replace the target context's state.""" root = context_data_dir(context_id) root.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(dir=root.parent) as directory: - with tarfile.open(json_path, "r:gz") as archive: - archive.extractall(directory, filter="data") - staged = Path(directory) / "state" - session_id = json.loads((staged / "snapshot-session.json").read_text())["session_id"] - if not isinstance(session_id, str) or not session_id: - raise ValueError("Snapshot has no conversation id") - # Memory repos and settings may contain absolute paths to the original context. - source = json.loads((staged / "context.json").read_text())["home"] - for path in staged.rglob("*.json"): - if ".git" not in path.parts: - path.write_text(path.read_text().replace(source, str(root))) - if root.exists(): - shutil.rmtree(root) - shutil.move(str(staged), root) + session_id, staged = await shielded_thread(stage_session, context_id, json_path, directory) + await shielded_thread(commit_staged_session, context_id, staged) return session_id @@ -271,11 +314,13 @@ def _descendants(pid: int) -> list[int]: @asynccontextmanager -async def model_relay(headers: dict[str, str]): +async def model_relay(headers: dict[str, str], on_forward=None): """Keep endpoint credentials and task attribution on requests to the configured model endpoint only.""" base = _base_url(os.environ) async with ClientSession(timeout=ClientTimeout(total=None), auto_decompress=False) as client: async def forward(request): + if on_forward is not None: + on_forward() outgoing = {"authorization": "Bearer " + os.environ.get("LITELLM_API_KEY", ""), **headers} if request.content_type: outgoing["content-type"] = request.headers.get("Content-Type", "application/json") @@ -292,7 +337,7 @@ async def forward(request): await response.write(chunk) await response.write_eof() return response - app = web.Application(client_max_size=100 * 1024**2) + app = web.Application(client_max_size=_RELAY_MAX_REQUEST_BYTES) app.router.add_route("*", "/{path:.*}", forward) runner = web.AppRunner(app) await runner.setup() @@ -306,7 +351,7 @@ async def forward(request): async def _command(env: dict[str, str], *args: str) -> bytes: - code, out, err = await _communicate("letta", "--backend", "local", *args, env=env, timeout_s=120) + code, out, err = await _communicate("letta", "--backend", "local", *args, env=env, timeout_s=_CLI_TIMEOUT_S) if code: raise RuntimeError(f"Letta Code {args[0]} failed: {err.decode(errors='replace')[-ERROR_MESSAGE_LIMIT:]}") return out @@ -380,7 +425,8 @@ async def _turn(prompt, context_id, session_id, model, mcp_servers, system_promp home = Path(env["HOME"]) (home / "context.json").write_text(json.dumps({"home": str(home)})) attribution = _attribution_headers(project_id, task_id) - async with model_relay(provider_headers(os.environ, attribution)) as relay: + async with model_relay(provider_headers(os.environ, attribution), + on_forward=lambda: _record_attribution(project_id, task_id)) as relay: await _command(env, "connect", "openai-compatible", "--base-url", relay, "--api-key", "relay") agent_file = home / "agent-id.json" if agent_file.exists(): @@ -414,7 +460,6 @@ async def _turn(prompt, context_id, session_id, model, mcp_servers, system_promp )) _RUNNING.add(proc) parser = _EventParser(event_callback) - _record_attribution(project_id, task_id) try: stderr = await _stream_cli(proc, parser.on_line) except BaseException: diff --git a/tst/unit/agents/letta_code/test_bridge.py b/tst/unit/agents/letta_code/test_bridge.py index 31d6200c5..a541af74d 100644 --- a/tst/unit/agents/letta_code/test_bridge.py +++ b/tst/unit/agents/letta_code/test_bridge.py @@ -97,7 +97,7 @@ async def wait(self): return self.returncode @asynccontextmanager - async def relay(*args): + async def relay(*args, **kwargs): yield "http://relay/v1" monkeypatch.setattr(bridge, "model_relay", relay) @@ -130,3 +130,100 @@ def test_direct_mcp_catalog_replaces_previous_context_tools(tmp_path): assert (tmp_path / ".letta/mods/agentenv-mcp.ts").exists() bridge._install_mcp_tools(tmp_path, "agent-local-1", []) assert json.loads(catalog.read_text())["tools"] == [] + + +def test_failed_workspace_swap_keeps_the_previous_conversation(monkeypatch, tmp_path): + monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path / "sessions")) + monkeypatch.setattr(a2a_server, "WORKSPACE_DIR", tmp_path / "app") + target = bridge.context_data_dir("target") + target.mkdir(parents=True) + (target / "conversation.json").write_text("previous") + (tmp_path / "app").mkdir() + (tmp_path / "app" / "kept.txt").write_text("previous workspace") + staged_state = tmp_path / "staged-state" + staged_state.mkdir() + (staged_state / "conversation.json").write_text("restored") + staged_workspace = tmp_path / "staged-workspace" + staged_workspace.mkdir() + (staged_workspace / "new.txt").write_text("restored workspace") + + def broken_move(*args, **kwargs): + if str(args[0]) == str(staged_state): + raise OSError("conversation move failed") + return real_move(*args, **kwargs) + + real_move = bridge.shutil.move + monkeypatch.setattr(bridge.shutil, "move", broken_move) + with pytest.raises(OSError): + a2a_server._replace_workspace(staged_workspace, lambda: bridge.commit_staged_session("target", staged_state)) + assert (tmp_path / "app" / "kept.txt").read_text() == "previous workspace" + assert not (tmp_path / "app" / "new.txt").exists() + assert (target / "conversation.json").read_text() == "previous" + + +def test_commit_staged_session_restores_previous_state_when_the_move_fails(monkeypatch, tmp_path): + monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path / "sessions")) + target = bridge.context_data_dir("target") + target.mkdir(parents=True) + (target / "conversation.json").write_text("previous") + staged = tmp_path / "staged" + staged.mkdir() + + def broken_move(*args, **kwargs): + raise OSError("move failed") + + monkeypatch.setattr(bridge.shutil, "move", broken_move) + with pytest.raises(OSError): + bridge.commit_staged_session("target", staged) + assert (target / "conversation.json").read_text() == "previous" + assert not target.with_name(target.name + ".previous").exists() + + +@pytest.mark.asyncio +async def test_cancelled_caller_waits_for_the_file_thread(): + finished = [] + release = __import__("threading").Event() + + def work(): + release.wait(5) + finished.append(True) + + task = asyncio.create_task(bridge.shielded_thread(work)) + await asyncio.sleep(0.05) + task.cancel() + await asyncio.sleep(0.05) + assert not task.done() + release.set() + with pytest.raises(asyncio.CancelledError): + await task + assert finished == [True] + + +@pytest.mark.asyncio +async def test_attribution_is_recorded_only_when_the_relay_forwards(monkeypatch): + monkeypatch.setitem(bridge._last_attribution, "project_id", None) + monkeypatch.setitem(bridge._last_attribution, "task_id", None) + seen = [] + + @asynccontextmanager + async def relay(headers, on_forward=None): + seen.append(on_forward) + yield "http://relay/v1" + + monkeypatch.setattr(bridge, "model_relay", relay) + + async def command(*args, **kwargs): + return b'{"id":"agent-local-1"}' + + async def spawn(*args, **kwargs): + raise OSError("cli did not start") + + monkeypatch.setattr(bridge, "_command", command) + monkeypatch.setattr(bridge, "_configure_agent", lambda *args: None) + monkeypatch.setattr(bridge.asyncio, "create_subprocess_exec", spawn) + with pytest.raises(OSError): + await bridge.run_letta_code("hi", "ctx-attr", project_id="p1", task_id="t1", workspace="/tmp") + assert bridge.get_last_attribution_snapshot()["project_id"] is None + seen[0]() + snapshot = bridge.get_last_attribution_snapshot() + assert snapshot["project_id"] == "p1" and snapshot["task_id"] == "t1" From b56209f2cb1841da586522bd5596f9692df43525 Mon Sep 17 00:00:00 2001 From: Letta Integration <300689746+letta-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:36:10 +0000 Subject: [PATCH 09/13] fix(agents): keep a restored Letta Code conversation when old-state cleanup fails Once the staged conversation is in place, removing the previous tree is best-effort cleanup, so a non-removable old file can no longer fail the commit and leave the new conversation paired with the old workspace. --- .../agents/letta-code/letta_code_bridge.py | 7 +++--- tst/unit/agents/letta_code/test_bridge.py | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py index 2a75941df..dbbe4c095 100644 --- a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py +++ b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py @@ -126,8 +126,7 @@ def commit_staged_session(context_id: str, staged: Path) -> None: root = context_data_dir(context_id) root.parent.mkdir(parents=True, exist_ok=True) previous = root.with_name(root.name + ".previous") - if previous.exists(): - shutil.rmtree(previous) + shutil.rmtree(previous, ignore_errors=True) if root.exists(): root.rename(previous) try: @@ -138,8 +137,8 @@ def commit_staged_session(context_id: str, staged: Path) -> None: if previous.exists(): previous.rename(root) raise - if previous.exists(): - shutil.rmtree(previous) + # The swap has succeeded; removing the old tree is cleanup and must not undo it. + shutil.rmtree(previous, ignore_errors=True) async def import_session(context_id: str, json_path: str) -> str: diff --git a/tst/unit/agents/letta_code/test_bridge.py b/tst/unit/agents/letta_code/test_bridge.py index a541af74d..c8cb1d6c1 100644 --- a/tst/unit/agents/letta_code/test_bridge.py +++ b/tst/unit/agents/letta_code/test_bridge.py @@ -227,3 +227,25 @@ async def spawn(*args, **kwargs): seen[0]() snapshot = bridge.get_last_attribution_snapshot() assert snapshot["project_id"] == "p1" and snapshot["task_id"] == "t1" + + +def test_cleanup_failure_after_a_successful_swap_keeps_the_new_conversation(monkeypatch, tmp_path): + monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path / "sessions")) + target = bridge.context_data_dir("target") + target.mkdir(parents=True) + (target / "conversation.json").write_text("previous") + staged = tmp_path / "staged" + staged.mkdir() + (staged / "conversation.json").write_text("restored") + real_rmtree = bridge.shutil.rmtree + + def rmtree(path, ignore_errors=False, **kwargs): + if str(path).endswith(".previous") and not ignore_errors: + raise PermissionError("old tree is read-only") + if str(path).endswith(".previous"): + return None + return real_rmtree(path, ignore_errors=ignore_errors, **kwargs) + + monkeypatch.setattr(bridge.shutil, "rmtree", rmtree) + bridge.commit_staged_session("target", staged) + assert (target / "conversation.json").read_text() == "restored" From d144b7fba4b3f27d7552d9beea502fa46b6eb77c Mon Sep 17 00:00:00 2001 From: Letta Integration <300689746+letta-integration[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:51:38 +0000 Subject: [PATCH 10/13] build(agents): pin Letta Code 0.34.26 0.34.26 ships @modelcontextprotocol/sdk 1.32.2, which resolves GHSA-46jr-4wx8-v453 and GHSA-6qxp-vccf-f47h. --- .../agents/agents/letta-code/README.md | 2 +- .../agents/letta-code/package-lock.json | 407 ++++++++++++------ .../agents/agents/letta-code/package.json | 2 +- 3 files changed, 279 insertions(+), 132 deletions(-) diff --git a/src/agent_env/agents/agents/letta-code/README.md b/src/agent_env/agents/agents/letta-code/README.md index 9017d070b..3c319b0d5 100644 --- a/src/agent_env/agents/agents/letta-code/README.md +++ b/src/agent_env/agents/agents/letta-code/README.md @@ -3,7 +3,7 @@ The [Letta Code](https://github.com/letta-ai/letta-code) CLI wrapped with `agentenv_protocol.a2a_agent`, shipped in the `agents` bundle as `@local/agentenv-framework/agents/letta-code`, with alias `letta-code`. This directory is the whole build context. The Dockerfile pins the Python base by digest and Node by version and checksum; -`package-lock.json` pins `@letta-ai/letta-code` to 0.34.25. +`package-lock.json` pins `@letta-ai/letta-code` to 0.34.26. Each turn runs `letta --backend local -p --output-format stream-json --yolo`. The bridge creates an agent with `letta agents create`; the first turn uses `--agent --new`, and subsequent turns use diff --git a/src/agent_env/agents/agents/letta-code/package-lock.json b/src/agent_env/agents/agents/letta-code/package-lock.json index 36635e93f..92848c884 100644 --- a/src/agent_env/agents/agents/letta-code/package-lock.json +++ b/src/agent_env/agents/agents/letta-code/package-lock.json @@ -5,23 +5,24 @@ "packages": { "": { "dependencies": { - "@letta-ai/letta-code": "0.34.25" + "@letta-ai/letta-code": "0.34.26" } }, "node_modules/@letta-ai/letta-code": { - "version": "0.34.25", - "resolved": "https://registry.npmjs.org/@letta-ai/letta-code/-/letta-code-0.34.25.tgz", - "integrity": "sha512-OrtcoobIWUikWOR6cJGQgZ7uma9fN4kjt8M+OxEtoMBIkaKV4EL7nQ2DWV6Br1rs+PH7vUpGcn/Xl0PKrDAyVw==", + "version": "0.34.26", + "resolved": "https://registry.npmjs.org/@letta-ai/letta-code/-/letta-code-0.34.26.tgz", + "integrity": "sha512-kt5rExmY21GXb9KZTceCnfxDsEiz0t/HieVVZcLOMnkvUwE8bbgDliJF02a+h5vaXVBt0t4eJYlyxEhYBdEBnA==", "hasInstallScript": true, "hasShrinkwrap": true, "license": "Apache-2.0", "dependencies": { + "@cursor/sdk": "1.1.0", "@earendil-works/pi-ai": "^0.99.1", "@janhapke/sharp-electron": "0.35.3-electron.1", "@letta-ai/letta-agent-sdk": "0.8.38", "@letta-ai/letta-client": "^1.10.2", "@letta-ai/trajectory": "0.2.0", - "@modelcontextprotocol/sdk": "1.30.0", + "@modelcontextprotocol/sdk": "1.32.2", "@pierre/diffs": "1.2.2", "@scarf/scarf": "^1.4.0", "@shikijs/langs": "4.0.2", @@ -107,15 +108,15 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/core": { - "version": "3.978.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.5.tgz", - "integrity": "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA==", + "version": "3.978.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.6.tgz", + "integrity": "sha512-ykF/MiYoM15dgZwF/WLgjJXVbAK2wU6vjPigeEnOHwyUJ0rdlraO5uekecKNVPkvOnz+oju/5zljViQs1nI1FQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", @@ -126,14 +127,14 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.76", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.76.tgz", - "integrity": "sha512-YxmL1Ar7bV9ZkJvtv+lj+MF6le76NnZnbmgySyZuxpZaCv0/1lZ3KxKy6RIj1h9ui2hWnVjZxbmTMeO0y/NrQA==", + "version": "3.972.77", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.77.tgz", + "integrity": "sha512-CN6dQCDo32a8n2gvm2HBeqGV5BNI9noG96nTj5bGO2HWtt0Rucu/A8pcnJIqkzILOHSuilnCk0Do8/7h6SOXhQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.5", + "@aws-sdk/core": "^3.978.6", "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -142,16 +143,16 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.78", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.78.tgz", - "integrity": "sha512-3VHknDRngoebsaqyT1iR75b5cBT2zfEtODK6yYmYltljkK1HEAQLs2FEd1IpWqAeOYdMGaG9wikngWLTufY7RA==", + "version": "3.972.79", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.79.tgz", + "integrity": "sha512-Netsu0nnmvP2HiN1ZFQBeUu+JehQIuubqz+j2kHDgeSE4HDzniCpIZ7RzoWABq+LTcbCi9W03hywifUagoVIWQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.5", + "@aws-sdk/core": "^3.978.6", "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/fetch-http-handler": "^5.8.0", - "@smithy/node-http-handler": "^4.12.1", + "@smithy/node-http-handler": "^4.13.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -159,22 +160,36 @@ "node": ">=20.0.0" } }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.13.0.tgz", + "integrity": "sha512-/GGcPoNJ2nzGytdAanpbx7mpp9Coa8LP+/Rpmbktr8YeLuIXs9B0SEz+X3AeonHdyzOIFJdKbN7jOZ+Tj+pMGw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.37.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.973.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.21.tgz", - "integrity": "sha512-aLx9hbCX6mGAyv4ZZd8UagJ4RASHVJw6BFDSIyuKmLkqNL6H47EjBPK6qaJ4B5TDV6mc+vMa0SJFYyo4mkvVQg==", + "version": "3.973.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.22.tgz", + "integrity": "sha512-x7/5h58cqOoZR0RWUZC7v/cJOADQR42iSIQkvU+00HVHR4jh8ZpqEgRPGf2zei59iH5qP40lL3bkq49PuWl71g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.5", - "@aws-sdk/credential-provider-env": "^3.972.76", - "@aws-sdk/credential-provider-http": "^3.972.78", - "@aws-sdk/credential-provider-login": "^3.972.83", - "@aws-sdk/credential-provider-process": "^3.972.76", - "@aws-sdk/credential-provider-sso": "^3.973.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.82", - "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/core": "^3.978.6", + "@aws-sdk/credential-provider-env": "^3.972.77", + "@aws-sdk/credential-provider-http": "^3.972.79", + "@aws-sdk/credential-provider-login": "^3.972.84", + "@aws-sdk/credential-provider-process": "^3.972.77", + "@aws-sdk/credential-provider-sso": "^3.973.21", + "@aws-sdk/credential-provider-web-identity": "^3.972.83", + "@aws-sdk/nested-clients": "^3.997.51", "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" @@ -184,15 +199,15 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.83", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.83.tgz", - "integrity": "sha512-9bJU6FjRacF4FK/QHf8Dyxhur7B1grRj8C1Yh4IkpoFW2exQflPdCv0VrbemLW9ZctLNle+AE7TETs9KNvuLYg==", + "version": "3.972.84", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.84.tgz", + "integrity": "sha512-G9Ty1uIV0laEvEp2KiMbEhsuBAjgaD3KYrwVjAM5VUxpcIuPBqQI5riA7/qzrnSgYiAKQbubbgkwcqCICKCIxg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.5", - "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/core": "^3.978.6", + "@aws-sdk/nested-clients": "^3.997.51", "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -201,19 +216,19 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.88", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.88.tgz", - "integrity": "sha512-AG0KID5qhQPzrFkh5b0Cws1Amytvzo0L0Xj6hMgJXEwD6ZXAqninDhSNAWJiN5zU4YVVZnuidnqwPwYJ04IamA==", + "version": "3.972.89", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.89.tgz", + "integrity": "sha512-O8xj7/sanrnXfFHu/LUujVBiiwn5mknYenU7xt1w6+yvhR1QoDZWXLFh1gLITaFVPtb1jWgEirC+kFNw+//tyw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.76", - "@aws-sdk/credential-provider-http": "^3.972.78", - "@aws-sdk/credential-provider-ini": "^3.973.21", - "@aws-sdk/credential-provider-process": "^3.972.76", - "@aws-sdk/credential-provider-sso": "^3.973.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.82", + "@aws-sdk/credential-provider-env": "^3.972.77", + "@aws-sdk/credential-provider-http": "^3.972.79", + "@aws-sdk/credential-provider-ini": "^3.973.22", + "@aws-sdk/credential-provider-process": "^3.972.77", + "@aws-sdk/credential-provider-sso": "^3.973.21", + "@aws-sdk/credential-provider-web-identity": "^3.972.83", "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" @@ -223,14 +238,14 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.76", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.76.tgz", - "integrity": "sha512-pdhKFTXv1zPYhh92MTlp0jVbLWvUEV9v+rziuEkoskGGQ2iJskxHTO38nA8UAbT/N4IM3n8OZJ9rIY5IBO15/A==", + "version": "3.972.77", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.77.tgz", + "integrity": "sha512-sWA8Jm+ASzkTxoqLizHWRbt8Gj3yZCmZ0hYdvg0uIxuLzWseYcfrDFzaSnkRWlEWSJV38rAWFimdrN0J4NR7Pg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.5", + "@aws-sdk/core": "^3.978.6", "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -239,16 +254,16 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.973.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.20.tgz", - "integrity": "sha512-DlxrAB3MnjNLx/da7b89W+3DyJQYso/q7Ns15MsST2IV2nkAYQOYTUCaffNSjgrpMP99Dm2mV2jxzcbyF94ZfA==", + "version": "3.973.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.21.tgz", + "integrity": "sha512-KV2LM8fbYUhDzGEBnPwb+ISNfNlhmnpLS+y6A6VsJpg1EI9aUJ0+dRTxh2GrGdneFH0T4TiuboHxz8i6+M38JA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.5", - "@aws-sdk/nested-clients": "^3.997.50", - "@aws-sdk/token-providers": "3.1155.0", + "@aws-sdk/core": "^3.978.6", + "@aws-sdk/nested-clients": "^3.997.51", + "@aws-sdk/token-providers": "3.1157.0", "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -257,15 +272,15 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.1155.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1155.0.tgz", - "integrity": "sha512-iIcQnYLhWuq9qOLQbIWFrxF3PF0svVeZvHnUS4DUnO4zUFmFHIyGyjqrntXG+e/5PUgfavV0pYfDIKHxTzN2LA==", + "version": "3.1157.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1157.0.tgz", + "integrity": "sha512-AeAzcwWRP2uANO2TjFpKjRdvOCzgfIu6YQl9Ceibd6E0gnq5dfOzujFnq2aeqgbLg4wRtXbUZE4gekCCPrz/OQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.5", - "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/core": "^3.978.6", + "@aws-sdk/nested-clients": "^3.997.51", "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -274,15 +289,15 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.82", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.82.tgz", - "integrity": "sha512-LUo46IqPTJ4xKBsrLuoaxiv/D2GTBCcFDMD1y3jKIqPR/p7Rw14RJFZ0iRWftqZtyLPQX9JMGnSi3mBu4y/pNA==", + "version": "3.972.83", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.83.tgz", + "integrity": "sha512-iekYilmo2Re51fC7gMkEfZ7jFDXwehSCmXjDESaYQsdgYhsLQFG7rTYr3GBvRIyj+Fkda2NxbZX7hAZDaFD0JQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.5", - "@aws-sdk/nested-clients": "^3.997.50", + "@aws-sdk/core": "^3.978.6", + "@aws-sdk/nested-clients": "^3.997.51", "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -291,13 +306,13 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.38", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.38.tgz", - "integrity": "sha512-K7vPKNstDlavHFZgbmcFdJOFIyowsevjddycRziRBbtXH/D9z+i9juCnH17liwSX9T8fJiDQzlEYkRtxkR+1vA==", + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.39.tgz", + "integrity": "sha512-V+AvQ+KFWApgm9134igy78cgoCEAFfV8K3KmlVi511vjkRWAgvy0tGUePrZsER+9EvrmUWEsrA2NBQspiGVBMQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -306,13 +321,13 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.33.tgz", - "integrity": "sha512-8QUt39D/dk5tdV7rLvIfMMI/ilQff4zth7mkMKpVOLM+M5AQ9W3nPxFVuHQANevmVWi4T5MlxqfKV4Q2drYeBA==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.34.tgz", + "integrity": "sha512-0IUoHM9N75uMaf70Tj2l+qmOSNw8HaDCsoghPRnewMaaPJjdSxzFaCpPBFDmSWuSlc0gBgv4CsvvvPGoBAD4Bg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -321,14 +336,14 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.58", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.58.tgz", - "integrity": "sha512-/BvocdloKozx/zFr4benH+9J1ywB/2dYFHPWNUdo0CBCe00gTm0HPflOq8MH6cuCTbnywSrscGq0/LheN59teQ==", + "version": "3.972.59", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.59.tgz", + "integrity": "sha512-lpLmEPrmRSG1ev+LqxkOn1JfVGclnS9b5hvGaSoBgS0JxW2u3QXsJuu+KTHvhdFrRohsqzl70cuiHhyUHFSjYQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.5", + "@aws-sdk/core": "^3.978.6", "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", @@ -339,17 +354,17 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/nested-clients": { - "version": "3.997.50", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.50.tgz", - "integrity": "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA==", + "version": "3.997.51", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.51.tgz", + "integrity": "sha512-DJK4iGIHWU8Jh8ODMvqFYcX9yGBMXV4tSZAvqlHF5EDd8hbmF3eFO9KIWOnA7wcQjPzEkyMMW1/FgZ9RU8oimQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.5", + "@aws-sdk/core": "^3.978.6", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", - "@smithy/core": "^3.36.1", + "@smithy/core": "^3.37.0", "@smithy/fetch-http-handler": "^5.8.0", - "@smithy/node-http-handler": "^4.12.1", + "@smithy/node-http-handler": "^4.13.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -357,6 +372,20 @@ "node": ">=20.0.0" } }, + "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.13.0.tgz", + "integrity": "sha512-/GGcPoNJ2nzGytdAanpbx7mpp9Coa8LP+/Rpmbktr8YeLuIXs9B0SEz+X3AeonHdyzOIFJdKbN7jOZ+Tj+pMGw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.37.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@letta-ai/letta-code/node_modules/@aws-sdk/signature-v4-multi-region": { "version": "3.996.49", "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.49.tgz", @@ -433,6 +462,136 @@ "node": ">=6.9.0" } }, + "node_modules/@letta-ai/letta-code/node_modules/@bufbuild/protobuf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@letta-ai/letta-code/node_modules/@connectrpc/connect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.7.0.tgz", + "integrity": "sha512-iNKdJRi69YP3mq6AePRT8F/HrxWCewrhxnLMNm0vpqXAR8biwzRtO6Hjx80C6UvtKJ5sFmffQT7I4Baecz389w==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@connectrpc/connect-web": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.7.0.tgz", + "integrity": "sha512-qyP0YOnUPRWwCc/VfsoydMJvkb7EyUPr2q9sHgBuJzbADjiqck1gKH5V5ZPzPhTLBvmz5UvG+wiZ5sMRQHU1MQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "1.7.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@cursor/sdk": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@cursor/sdk/-/sdk-1.1.0.tgz", + "integrity": "sha512-QrqxwfgbT7e1oOsXzs0O86eGdPKA4VPOGU+v4vesZqRsPUIH/QKStWiE8/kV7eNQm1hEi9y2ijbeniZeXozgNA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@bufbuild/protobuf": "1.10.0", + "@connectrpc/connect": "^1.6.1", + "@connectrpc/connect-web": "^1.6.1", + "@statsig/js-client": "3.31.0" + }, + "engines": { + "node": ">=22.13" + }, + "optionalDependencies": { + "@cursor/sdk-darwin-arm64": "1.1.0", + "@cursor/sdk-darwin-x64": "1.1.0", + "@cursor/sdk-linux-arm64": "1.1.0", + "@cursor/sdk-linux-x64": "1.1.0", + "@cursor/sdk-win32-x64": "1.1.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@cursor/sdk-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-arm64/-/sdk-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-3ii4Yc4KtiHklMUGN6erAfJfDk6qYDi2lWdcj2NPFwidnGcRVrM5a2C28aG5wuqQPZrezdhD7zVYK/BmgwD3VQ==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "rg": "bin/rg" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@cursor/sdk-darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-x64/-/sdk-darwin-x64-1.1.0.tgz", + "integrity": "sha512-hjRjNrnjqdom8/wUP00DAXttTSYxnKr8ZAbFZ0vf+Mo4zo6YtjTU0sewoadtuT9T0WGH2fI5SfZsSXyWMWOmqg==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "rg": "bin/rg" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@cursor/sdk-linux-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@cursor/sdk-linux-arm64/-/sdk-linux-arm64-1.1.0.tgz", + "integrity": "sha512-olTKTyselLXGc06ehPtvoCFbSvZ3nZYh0J08b3e49YxdiOrXAIyE9IFD8tsOVaGkjHZISOL2PRWFBsOA03QmDQ==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "rg": "bin/rg" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@cursor/sdk-linux-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@cursor/sdk-linux-x64/-/sdk-linux-x64-1.1.0.tgz", + "integrity": "sha512-/gd/LRroo7qFDCFLnJOHc8HIEYQt020tFUZtdt6WB+SNGIeyUtflTqO97c1axU6IvH75QaNksMBdT0OEz4kNZg==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "rg": "bin/rg" + } + }, + "node_modules/@letta-ai/letta-code/node_modules/@cursor/sdk-win32-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@cursor/sdk-win32-x64/-/sdk-win32-x64-1.1.0.tgz", + "integrity": "sha512-pL1hQVaUPWEe7ReH6tqRY76BPASd58M0k1hJbXL00EFRtlnSoeDRCvVvOApvOQN/FBdP9DNbgqdWmETODc4jDQ==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ], + "bin": { + "rg": "bin/rg.exe" + } + }, "node_modules/@letta-ai/letta-code/node_modules/@earendil-works/pi-ai": { "version": "0.99.2", "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.99.2.tgz", @@ -1586,33 +1745,6 @@ "react-dom": "^18.3.1 || ^19.0.0" } }, - "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/@pierre/diffs/node_modules/@shikijs/langs": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", - "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.23.0" - } - }, - "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/@pierre/diffs/node_modules/shiki": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", - "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@shikijs/core": "3.23.0", - "@shikijs/engine-javascript": "3.23.0", - "@shikijs/engine-oniguruma": "3.23.0", - "@shikijs/langs": "3.23.0", - "@shikijs/themes": "3.23.0", - "@shikijs/types": "3.23.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, "node_modules/@letta-ai/letta-code/node_modules/@letta-ai/letta-code/node_modules/@shikijs/core": { "version": "3.23.0", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", @@ -1713,9 +1845,9 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/@modelcontextprotocol/sdk": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", - "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "version": "1.32.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.2.tgz", + "integrity": "sha512-SWsVtH/0vULSQStoZ6xxH2K09Li8fXYTMC64ZeogCdpZ3XSi1eCPOByV1OpuJxuI3OcmHhosDWYyii07tpPsLw==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", @@ -2041,9 +2173,9 @@ "license": "MIT" }, "node_modules/@letta-ai/letta-code/node_modules/@smithy/core": { - "version": "3.36.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.36.2.tgz", - "integrity": "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ==", + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.37.0.tgz", + "integrity": "sha512-97imTcCyiPd8k0e9+ts/Sp0W65Dn9Ti357J7M09Ov4tI8Q1JDJVFSoAJtjEVKqfHEr3Pdo2jfolqKCaaiszH0g==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.19.0", @@ -2127,6 +2259,21 @@ "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", "license": "MIT" }, + "node_modules/@letta-ai/letta-code/node_modules/@statsig/client-core": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@statsig/client-core/-/client-core-3.31.0.tgz", + "integrity": "sha512-SuxQD6TmVszPG7FoMKwTk/uyBuVFk7XnxI3T/E0uyb7PL7GNjONtfsoh+NqBBVUJVse0CUeSFfgJPoZy1ZOslQ==", + "license": "ISC" + }, + "node_modules/@letta-ai/letta-code/node_modules/@statsig/js-client": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@statsig/js-client/-/js-client-3.31.0.tgz", + "integrity": "sha512-LFa5E0LjT6sTfZv3sNGoyRLSZ1078+agdgOA+Vm1ecjG+KbSOfBLTW7hMwimrJ29slRwbYDzbtKaPJo/R37N2g==", + "license": "ISC", + "dependencies": { + "@statsig/client-core": "3.31.0" + } + }, "node_modules/@letta-ai/letta-code/node_modules/@types/hast": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", @@ -2531,9 +2678,9 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/bowser": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.15.0.tgz", + "integrity": "sha512-xic6KSZglg/I4ymp45fdM4GWfYpBK79Fxd/TOvJktlaPgQqqDxvmGwOPZl2V0dyeS3Lj5c180bLhXF43iyAjZQ==", "license": "MIT" }, "node_modules/@letta-ai/letta-code/node_modules/brace-expansion": { @@ -4464,9 +4611,9 @@ } }, "node_modules/@letta-ai/letta-code/node_modules/qs": { - "version": "6.16.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", - "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.17.0.tgz", + "integrity": "sha512-5LK5BZ2XDHMvcUJaf0CQ5HL3aVgKbpklvByC7qLSxYkfE8COEEbJg5xCHWCTqzqi0Gs4Jn5Mv5v3R38W6edscA==", "license": "BSD-3-Clause", "dependencies": { "es-define-property": "^1.0.1", diff --git a/src/agent_env/agents/agents/letta-code/package.json b/src/agent_env/agents/agents/letta-code/package.json index e5c707973..397e542c0 100644 --- a/src/agent_env/agents/agents/letta-code/package.json +++ b/src/agent_env/agents/agents/letta-code/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@letta-ai/letta-code": "0.34.25" + "@letta-ai/letta-code": "0.34.26" } } From b66a98d287ce9cab89ffb5adbfc7dc48c3828307 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 28 Oct 2026 08:27:58 -0700 Subject: [PATCH 11/13] fix(agents): finish Letta Code restores and changelog writes before a cancel leaves them A snapshot load cancelled during its swap now still binds the context to the conversation it restored: the swap and the binding run in one shielded step. Every file thread that runs under the workspace or changelog lock is shielded, so a cancel no longer releases the lock, or removes a temporary directory, while the thread still writes. The MCP tool mod names its call timeout and output limit. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../agents/agents/letta-code/a2a_server.py | 35 +++++----- .../agents/letta-code/letta_code_bridge.py | 11 +++- .../agents/agents/letta-code/mcp_tools.ts | 7 +- tst/unit/agents/letta_code/test_bridge.py | 66 +++++++++++++++++++ 4 files changed, 100 insertions(+), 19 deletions(-) diff --git a/src/agent_env/agents/agents/letta-code/a2a_server.py b/src/agent_env/agents/agents/letta-code/a2a_server.py index d3bdd762b..837d3395f 100644 --- a/src/agent_env/agents/agents/letta-code/a2a_server.py +++ b/src/agent_env/agents/agents/letta-code/a2a_server.py @@ -72,6 +72,7 @@ export_session, get_last_attribution_snapshot, kill_running_clis, + shielded, shielded_thread, stage_session, run_letta_code, @@ -422,7 +423,7 @@ async def save_snapshot(self, request: ObjectSnapshotSaveRequest) -> dict[str, A raise HTTPException(status_code=404, detail="No active session for context_id") session_json = await export_session(request.context_id, session_id) if request.objects.workspace is not None: - await asyncio.to_thread( + await shielded_thread( subprocess.run, ["tar", *_backup_exclusions(), "-czf", str(workspace), "-C", str(WORKSPACE_DIR), "."], @@ -445,14 +446,15 @@ async def load_snapshot(self, request: ObjectSnapshotLoadRequest) -> dict[str, A staged = Path(directory) / "workspace" staged.mkdir() await download(request.objects.workspace, archive) - await asyncio.to_thread( + await shielded_thread( subprocess.run, ["tar", "-xzf", str(archive), "-C", str(staged)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, ) async with self._workspace.exclusive(), self._turn_lock(target_context_id): # Stage the conversation first, so a bad snapshot changes nothing. The workspace swap and the - # conversation swap then run in one thread that cancellation can't abandon: a failed conversation - # swap puts /app back, and the context's previous conversation stays in place until both succeed. + # conversation swap then run in one thread, and the context is bound to the restored conversation, + # in one step that cancellation can't abandon: a failed conversation swap puts /app back, and the + # context's previous conversation stays in place until both succeed. session_dir = Path(directory) / "session" session_dir.mkdir() imported_session_id, staged_session = await shielded_thread( @@ -463,11 +465,14 @@ async def load_snapshot(self, request: ObjectSnapshotLoadRequest) -> dict[str, A def commit_session() -> None: commit_staged_session(target_context_id, staged_session) - if staged is not None: - await shielded_thread(_replace_workspace, staged, commit_session) - else: - await shielded_thread(commit_session) - self.set_session_ref_for_context(target_context_id, imported_session_id) + async def swap() -> None: + if staged is not None: + await asyncio.to_thread(_replace_workspace, staged, commit_session) + else: + await asyncio.to_thread(commit_session) + self.set_session_ref_for_context(target_context_id, imported_session_id) + + await shielded(swap()) return {"context_id": target_context_id} async def _capture_changelog(self) -> None: @@ -488,11 +493,11 @@ async def _capture_changelog(self) -> None: pending_snar = _CHANGELOG_DIR / "pending.snar" record = _CHANGELOG_META_DIR / f"{sequence:06d}" try: - await asyncio.to_thread(shutil.rmtree, record, True) + await shielded_thread(shutil.rmtree, record, True) record.mkdir(parents=True) (record / _META_NAME).write_text(json.dumps({"sequence": sequence, "removed": removed})) - await asyncio.to_thread(shutil.copyfile, _CHANGELOG_SNAR, pending_snar) - await asyncio.to_thread( + await shielded_thread(shutil.copyfile, _CHANGELOG_SNAR, pending_snar) + await shielded_thread( subprocess.run, ["tar", f"--listed-incremental={pending_snar}", *_changelog_backup_exclusions(), @@ -508,7 +513,7 @@ async def _capture_changelog(self) -> None: finally: archive.unlink(missing_ok=True) pending_snar.unlink(missing_ok=True) - await asyncio.to_thread(shutil.rmtree, record, True) + await shielded_thread(shutil.rmtree, record, True) class LettaCodeChangelogAgent(LettaCodeAgent): @@ -520,7 +525,7 @@ class LettaCodeChangelogAgent(LettaCodeAgent): async def enable_changelog(self, request: NamespaceChangelogEnableRequest) -> dict[str, Any]: roots = request.roots or [str(WORKSPACE_DIR)] async with self._workspace.exclusive(), self._changelog_lock: - await asyncio.to_thread(_baseline_changelog, roots) + await shielded_thread(_baseline_changelog, roots) self._changelog = {"roots": roots, "sequence": 0, "present": [root for root in roots if Path(root).exists()], "uploader": NamespaceUploader(request.write_namespace)} return {"roots": roots} @@ -532,7 +537,7 @@ async def apply_changelog(self, request: ObjectChangelogApplyRequest) -> dict[st for increment in request.increments: archive = Path(directory) / f"{increment.sequence:06d}.tgz" await download(increment.object, archive) - await asyncio.to_thread(_apply_increment, archive, increment.sequence) + await shielded_thread(_apply_increment, archive, increment.sequence) return {"count": len(request.increments), "context_id": request.target_context_id} diff --git a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py index dbbe4c095..681f175cf 100644 --- a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py +++ b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py @@ -93,9 +93,9 @@ async def export_session(context_id: str, session_id: str) -> bytes: return await shielded_thread(_archive_session, context_id, session_id) -async def shielded_thread(func, *args): - """Run blocking file work in a thread that cancellation can't abandon: a cancelled caller still waits for it.""" - task = asyncio.ensure_future(asyncio.to_thread(func, *args)) +async def shielded(work): + """Await work that cancellation can't abandon: a cancelled caller still waits for it to finish.""" + task = asyncio.ensure_future(work) try: return await asyncio.shield(task) except asyncio.CancelledError: @@ -104,6 +104,11 @@ async def shielded_thread(func, *args): raise +async def shielded_thread(func, *args, **kwargs): + """Run blocking file work in a thread that cancellation can't abandon: a cancelled caller still waits for it.""" + return await shielded(asyncio.to_thread(func, *args, **kwargs)) + + def stage_session(context_id: str, json_path: str, directory: str) -> tuple[str, Path]: """Extract a snapshot's local state into directory, with paths rewritten for context_id; the target is untouched.""" root = context_data_dir(context_id) diff --git a/src/agent_env/agents/agents/letta-code/mcp_tools.ts b/src/agent_env/agents/agents/letta-code/mcp_tools.ts index 688c910ba..81cea17cc 100644 --- a/src/agent_env/agents/agents/letta-code/mcp_tools.ts +++ b/src/agent_env/agents/agents/letta-code/mcp_tools.ts @@ -4,6 +4,11 @@ import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; +// How long one `letta mcp call` may run, as long as the bridge gives its other CLI commands (_CLI_TIMEOUT_S). +const MCP_CALL_TIMEOUT_MS = 120_000; +// The most output one `letta mcp call` may print; a larger tool result fails the call. +const MCP_CALL_MAX_OUTPUT_BYTES = 16 * 1024 * 1024; + export default function activate(letta) { const { agentId, tools } = JSON.parse( readFileSync(join(homedir(), ".letta", "agentenv-mcp-tools.json"), "utf8"), @@ -18,7 +23,7 @@ export default function activate(letta) { return new Promise((resolve) => { execFile("letta", ["--backend", "local", "mcp", "call", tool.name, "--agent", agentId, "--args", JSON.stringify(ctx.args)], - { signal: ctx.signal, timeout: 120000, maxBuffer: 16 * 1024 * 1024 }, + { signal: ctx.signal, timeout: MCP_CALL_TIMEOUT_MS, maxBuffer: MCP_CALL_MAX_OUTPUT_BYTES }, (error, stdout, stderr) => { let result; try { result = JSON.parse(stdout); } catch { /* CLI startup errors have only stderr. */ } diff --git a/tst/unit/agents/letta_code/test_bridge.py b/tst/unit/agents/letta_code/test_bridge.py index c8cb1d6c1..bd5e4e980 100644 --- a/tst/unit/agents/letta_code/test_bridge.py +++ b/tst/unit/agents/letta_code/test_bridge.py @@ -1,8 +1,11 @@ """Exercise state restore, system instructions, event parsing and cancellation without model calls.""" import asyncio import json +import shutil import subprocess +import threading from contextlib import asynccontextmanager +from types import SimpleNamespace import pytest @@ -249,3 +252,66 @@ def rmtree(path, ignore_errors=False, **kwargs): monkeypatch.setattr(bridge.shutil, "rmtree", rmtree) bridge.commit_staged_session("target", staged) assert (target / "conversation.json").read_text() == "restored" + + +@pytest.mark.asyncio +async def test_a_load_cancelled_during_its_swap_still_binds_the_restored_conversation(monkeypatch, tmp_path): + monkeypatch.setattr(bridge, "SESSIONS_ROOT", str(tmp_path / "sessions")) + source = bridge.context_data_dir("source") + source.mkdir(parents=True) + (source / "context.json").write_text(json.dumps({"home": str(source)})) + snapshot = tmp_path / "state.tgz" + snapshot.write_bytes(await bridge.export_session("source", "local-conv-7")) + + async def download(_object, target): + shutil.copyfile(snapshot, target) + + swapping, release = threading.Event(), threading.Event() + real_commit = a2a_server.commit_staged_session + + def commit(context_id, staged): + swapping.set() + release.wait(5) + real_commit(context_id, staged) + + monkeypatch.setattr(a2a_server, "download", download) + monkeypatch.setattr(a2a_server, "commit_staged_session", commit) + agent = a2a_server.LettaCodeAgent() + a2a_server.build_app(agent) + request = SimpleNamespace(target_context_id="target", objects=SimpleNamespace(trajectory=None, workspace=None)) + load = asyncio.create_task(agent.load_snapshot(request)) + assert await asyncio.to_thread(swapping.wait, 5) + load.cancel() + release.set() + with pytest.raises(asyncio.CancelledError): + await load + # The swap finished, so the context resumes the conversation it restored, not the one it had before. + assert agent.session_ref_for_context("target") == "local-conv-7" + + +@pytest.mark.asyncio +async def test_a_cancelled_changelog_apply_holds_the_workspace_until_its_extract_ends(monkeypatch): + applying, release = threading.Event(), threading.Event() + + def apply(_archive, _sequence): + applying.set() + release.wait(5) + + async def download(_object, target): + target.write_bytes(b"") + + monkeypatch.setattr(a2a_server, "_apply_increment", apply) + monkeypatch.setattr(a2a_server, "download", download) + agent = a2a_server.LettaCodeChangelogAgent() + a2a_server.build_app(agent) + request = SimpleNamespace(target_context_id="ctx", increments=[SimpleNamespace(sequence=0, object=None)]) + task = asyncio.create_task(agent.apply_changelog(request)) + assert await asyncio.to_thread(applying.wait, 5) + task.cancel() + await asyncio.sleep(0.05) + # No turn may start in the workspace while the extract still writes to it. + assert not task.done() and agent._workspace._writer + release.set() + with pytest.raises(asyncio.CancelledError): + await task + assert not agent._workspace._writer From 196a183f6543cce93e1dbe5f0085050dff8243b4 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 28 Oct 2026 09:18:27 -0700 Subject: [PATCH 12/13] perf(agents): list only the turn's model to the Letta Code CLI The CLI merges each model an endpoint lists into its own list with a scan of them all, and it does so in every command that loads models. Over an endpoint that lists 3006 models that took about 45 s of CPU in agents create and again in every turn's run, so a one-word reply took 50-98 s and the validator's concurrent prompts timed out at 120 s. The per-turn relay now answers GET /v1/models with the turn's model alone, without asking the endpoint: a turn takes about 4 s. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../agents/agents/letta-code/README.md | 4 +++- .../agents/letta-code/letta_code_bridge.py | 10 +++++++--- tst/unit/agents/letta_code/test_bridge.py | 17 ++++++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/agent_env/agents/agents/letta-code/README.md b/src/agent_env/agents/agents/letta-code/README.md index 3c319b0d5..07698dd13 100644 --- a/src/agent_env/agents/agents/letta-code/README.md +++ b/src/agent_env/agents/agents/letta-code/README.md @@ -13,7 +13,9 @@ run concurrently; turns in one context and snapshot operations are synchronized. The bridge connects the local backend with `letta connect openai-compatible`. A per-turn loopback relay forwards requests to `$LITELLM_BASE_URL/v1`, with `LITELLM_API_KEY`, `[model.headers]` (`LITELLM_EXTRA_HEADERS`), and `project_id` / `task_id` as `x-litellm-customer-id` and `x-litellm-tags`. The model defaults to -`openai/gpt-4.1-mini`. Endpoint credentials stay at the relay. Registered MCP servers retain their names and headers; +`openai/gpt-4.1-mini`. Endpoint credentials stay at the relay, which answers the CLI's model list with the turn's model +alone: the CLI's merge of a listed model scans every model listed, which costs about 45 s of CPU per command over an +endpoint that lists thousands. Registered MCP servers retain their names and headers; a task role replaces `AgentEnv-Role`. Before each turn, `letta mcp tools --full` discovers the exact schemas. A context-local mod registers them as direct Letta Code tools through `letta.tools.register`; each tool executes `letta mcp call` with its full `mcp____` name. Peer tools use the same path. diff --git a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py index 681f175cf..79d5c8cc0 100644 --- a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py +++ b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py @@ -318,11 +318,15 @@ def _descendants(pid: int) -> list[int]: @asynccontextmanager -async def model_relay(headers: dict[str, str], on_forward=None): - """Keep endpoint credentials and task attribution on requests to the configured model endpoint only.""" +async def model_relay(headers: dict[str, str], model: str, on_forward=None): + """Keep endpoint credentials and task attribution on requests to the configured model endpoint only. The model + list is the turn's model alone: the CLI merges a listed model into its own with a scan of them all, which costs + minutes of CPU per command over an endpoint that lists thousands.""" base = _base_url(os.environ) async with ClientSession(timeout=ClientTimeout(total=None), auto_decompress=False) as client: async def forward(request): + if request.method == "GET" and request.path.rstrip("/") == "/v1/models": + return web.json_response({"object": "list", "data": [{"id": model, "object": "model"}]}) if on_forward is not None: on_forward() outgoing = {"authorization": "Bearer " + os.environ.get("LITELLM_API_KEY", ""), **headers} @@ -429,7 +433,7 @@ async def _turn(prompt, context_id, session_id, model, mcp_servers, system_promp home = Path(env["HOME"]) (home / "context.json").write_text(json.dumps({"home": str(home)})) attribution = _attribution_headers(project_id, task_id) - async with model_relay(provider_headers(os.environ, attribution), + async with model_relay(provider_headers(os.environ, attribution), model, on_forward=lambda: _record_attribution(project_id, task_id)) as relay: await _command(env, "connect", "openai-compatible", "--base-url", relay, "--api-key", "relay") agent_file = home / "agent-id.json" diff --git a/tst/unit/agents/letta_code/test_bridge.py b/tst/unit/agents/letta_code/test_bridge.py index bd5e4e980..07c4cd7bb 100644 --- a/tst/unit/agents/letta_code/test_bridge.py +++ b/tst/unit/agents/letta_code/test_bridge.py @@ -8,6 +8,7 @@ from types import SimpleNamespace import pytest +from pytest_socket import enable_socket from tst.unit.agents.letta_code.agent import a2a_server, letta_code_bridge as bridge @@ -209,7 +210,7 @@ async def test_attribution_is_recorded_only_when_the_relay_forwards(monkeypatch) seen = [] @asynccontextmanager - async def relay(headers, on_forward=None): + async def relay(headers, model, on_forward=None): seen.append(on_forward) yield "http://relay/v1" @@ -315,3 +316,17 @@ async def download(_object, target): with pytest.raises(asyncio.CancelledError): await task assert not agent._workspace._writer + + +@pytest.mark.asyncio +async def test_the_relay_lists_only_the_turns_model_and_forwards_nothing_for_it(monkeypatch): + enable_socket() # the relay listens on loopback + # An endpoint that refuses every connection: the list must not come from it. + monkeypatch.setenv("LITELLM_BASE_URL", "http://127.0.0.1:9") + forwarded = [] + async with bridge.model_relay({}, "openai/gpt-4.1-mini", on_forward=lambda: forwarded.append(True)) as relay: + async with bridge.ClientSession() as client: + async with client.get(relay + "/models") as response: + listed = await response.json() + assert listed == {"object": "list", "data": [{"id": "openai/gpt-4.1-mini", "object": "model"}]} + assert forwarded == [] From 8cb6e00446575ec0c8b277324ff735044b5374f8 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 28 Oct 2026 09:37:53 -0700 Subject: [PATCH 13/13] fix(agents): hold Letta Code's shield through a repeated cancel, and name its id lengths shielded waited on its work unshielded after the first cancel, so a second cancel reached the work itself: a load could release the workspace lock, and skip binding its conversation, while its thread still wrote. The wait is now shielded until the work is done. The context dir's hash length and an attachment's prefix length are named. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../agents/agents/letta-code/a2a_server.py | 4 +++- .../agents/letta-code/letta_code_bridge.py | 14 +++++++++---- tst/unit/agents/letta_code/test_bridge.py | 21 +++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/agent_env/agents/agents/letta-code/a2a_server.py b/src/agent_env/agents/agents/letta-code/a2a_server.py index 837d3395f..57519820b 100644 --- a/src/agent_env/agents/agents/letta-code/a2a_server.py +++ b/src/agent_env/agents/agents/letta-code/a2a_server.py @@ -108,6 +108,8 @@ _PDF_RENDER_TIMEOUT_S = 60 _PDF_RENDER_DPI = 150 _SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9._-]") +# Random hex digits before an attachment's name when the task's inputs folder already holds a file of that name. +_ATTACHMENT_PREFIX_CHARS = 8 # The versioned format of the `letta -p --output-format stream-json` events this agent appends to its trajectory. TRAJECTORY_FORMAT = "letta-code-events/1" @@ -180,7 +182,7 @@ def _materialize_and_render(part: FilePart, task_id: str) -> list[str]: name = _safe_name(part.name) target = directory / name if target.exists(): - target = directory / f"{uuid.uuid4().hex[:8]}-{name}" + target = directory / f"{uuid.uuid4().hex[:_ATTACHMENT_PREFIX_CHARS]}-{name}" if part.bytes is not None: try: target.write_bytes(base64.b64decode(part.bytes, validate=True)) diff --git a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py index 79d5c8cc0..e6aa5e174 100644 --- a/src/agent_env/agents/agents/letta-code/letta_code_bridge.py +++ b/src/agent_env/agents/agents/letta-code/letta_code_bridge.py @@ -36,6 +36,9 @@ # Root under which each A2A context_id gets its own Letta Code data dir; lives # under $HOME so host-mode instances don't share session state. SESSIONS_ROOT = os.path.join(os.environ.get("HOME", "/data"), "sessions") +# How many hex digits of the context id's SHA-1 name its data dir: short, and unique across any realistic number of +# contexts on one host. +_CONTEXT_DIR_HASH_CHARS = 16 _CLI_TIMEOUT_S = 120 # Largest model request body the relay accepts from the CLI. _RELAY_MAX_REQUEST_BYTES = 100 * 1024**2 @@ -50,7 +53,7 @@ def _base_url(environ: Mapping[str, str]) -> str: def context_data_dir(context_id: str) -> Path: """Per-context Letta Code data dir. A stable short hash keeps the path clean and filesystem-safe regardless of the context_id's charset.""" - ctx_hash = hashlib.sha1(context_id.encode()).hexdigest()[:16] + ctx_hash = hashlib.sha1(context_id.encode()).hexdigest()[:_CONTEXT_DIR_HASH_CHARS] return Path(SESSIONS_ROOT) / ctx_hash @@ -99,9 +102,12 @@ async def shielded(work): try: return await asyncio.shield(task) except asyncio.CancelledError: - with contextlib.suppress(BaseException): - await task - raise + # A cancel that arrives while it waits is held back too: an unshielded wait would pass it on to the work. + while True: + with contextlib.suppress(BaseException): + await asyncio.shield(task) + if task.done(): + raise async def shielded_thread(func, *args, **kwargs): diff --git a/tst/unit/agents/letta_code/test_bridge.py b/tst/unit/agents/letta_code/test_bridge.py index 07c4cd7bb..68ad23f9b 100644 --- a/tst/unit/agents/letta_code/test_bridge.py +++ b/tst/unit/agents/letta_code/test_bridge.py @@ -330,3 +330,24 @@ async def test_the_relay_lists_only_the_turns_model_and_forwards_nothing_for_it( listed = await response.json() assert listed == {"object": "list", "data": [{"id": "openai/gpt-4.1-mini", "object": "model"}]} assert forwarded == [] + + +@pytest.mark.asyncio +async def test_a_caller_cancelled_twice_still_waits_for_the_work_and_its_last_step(): + release = threading.Event() + finished = [] + + async def work(): + await asyncio.to_thread(release.wait, 5) + finished.append(True) + + task = asyncio.create_task(bridge.shielded(work())) + await asyncio.sleep(0.05) + for _ in range(2): + task.cancel() + await asyncio.sleep(0.05) + assert not task.done() + release.set() + with pytest.raises(asyncio.CancelledError): + await task + assert finished == [True]