Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions .github/scripts/infra_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -73,6 +74,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"'],
Expand Down
8 changes: 8 additions & 0 deletions src/agent_env/agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ OpenAI-compatible one, `<base>/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
Expand Down
3 changes: 3 additions & 0 deletions src/agent_env/agents/agents/letta-code/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
__pycache__
*.pyc
38 changes: 38 additions & 0 deletions src/agent_env/agents/agents/letta-code/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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.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/*; \
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 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 \
&& 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
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"]
41 changes: 41 additions & 0 deletions src/agent_env/agents/agents/letta-code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 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.26.

Each turn runs `letta --backend local -p --output-format stream-json --yolo`. The bridge creates an agent with
`letta agents create`; the first turn uses `--agent <id> --new`, and subsequent turns use
`--conversation <id>`. 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, which answers the CLI's model list with the turn's model
alone: the CLI's merge of a listed model scans every model listed, which costs about 45 s of CPU per command over an
endpoint that lists thousands. Registered MCP servers retain their names and headers;
a task role replaces `AgentEnv-Role`. Before each turn, `letta mcp tools --full` discovers the exact schemas. A context-local mod registers them as
direct Letta Code tools through `letta.tools.register`; each tool executes `letta mcp call` with its full
`mcp__<server>__<tool>` 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 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. |
| `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.
Loading
Loading