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 1/9] 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 d9a8cd42..720aa4bf 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 00000000..513f7394 --- /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 00000000..0c12e79c --- /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 00000000..e295c5de --- /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 00000000..eb214936 --- /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 00000000..c59e6c1c --- /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 00000000..1301e990 --- /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 00000000..500c3036 --- /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 00000000..d38eb063 --- /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 00000000..36635e93 --- /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 00000000..e5c70797 --- /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 00000000..bed7e895 --- /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 00000000..a40bd7c2 --- /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 02d350ed..03725f23 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 00000000..7efb0ace --- /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 00000000..b29d366a --- /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 2/9] 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 e295c5de..4fb6fe9e 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 500c3036..4f3bf2d7 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 00000000..bb4c561f --- /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 3/9] 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 44495505..e42496f2 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 4/9] 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 fde20cd9..9b35b395 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 4826777d..121caa52 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 97c54904..e44825ac 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 5/9] 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 0c12e79c..6ead4f69 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 4fb6fe9e..9017d070 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 1301e990..7197ad11 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 4f3bf2d7..0059ce5e 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 00000000..688c910b --- /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 bb4c561f..31d6200c 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 6/9] 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 6146dee2..41a741e6 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 720aa4bf..f9a37b74 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 9017d070..ec04bcd3 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 eb214936..622db785 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 0059ce5e..b6b54605 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 1445a0b3..35eb44cf 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 952d931d..a7a9972e 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 f2b3728b..8290ceed 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 00000000..ef7152ae --- /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 7/9] 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 41a741e6..6146dee2 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 f9a37b74..720aa4bf 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 ec04bcd3..9017d070 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 622db785..eb214936 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 b6b54605..0059ce5e 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 35eb44cf..1445a0b3 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 a7a9972e..952d931d 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 8290ceed..f2b3728b 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 ef7152ae..00000000 --- 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 8/9] 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 6ead4f69..53cc4b5a 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 eb214936..d3bdd762 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 0059ce5e..2a75941d 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 31d6200c..a541af74 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 9/9] 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 2a75941d..dbbe4c09 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 a541af74..c8cb1d6c 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"