Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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"'],
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 @@ -61,6 +61,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"]
39 changes: 39 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,39 @@
# 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 <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. 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