Skip to content

feat(agents): add a Letta Code agent - #345

Closed
sarahwooders wants to merge 11 commits into
scaleapi:mainfrom
letta-ai:add-letta-code-agent
Closed

sarahwooders wants to merge 11 commits into
scaleapi:mainfrom
letta-ai:add-letta-code-agent

Conversation

@sarahwooders

@sarahwooders sarahwooders commented Oct 27, 2026 •

Copy link
Copy Markdown

What and why

Adds agents/letta-code/, a Letta Code agent built the same way as the other CLI agents, alongside OpenCode. Tasks can name it as letta-code (alias in aliases.toml) or as @local/agentenv-framework/agents/letta-code.

  • How it runs: each turn is one headless letta --backend local -p ... --output-format stream-json run in /app, and its stream-json events are the turn's trajectory.
  • Model endpoint: the local backend calls the task's [model] endpoint, $LITELLM_BASE_URL/v1, as an OpenAI-compatible provider through a per-turn relay that adds [model.headers] and the run's attribution. No Letta account or server is needed.
  • Memory across turns: a context's first task creates a Letta agent and conversation, and later tasks resume the same conversation. The agent's memory therefore persists across a context's turns, and snapshot/v1 save/load carries it to another context.
  • MCP: the task env's MCP servers, including peer_agents, are exposed to the model as direct tools.
  • Image and release: Node and the CLI are pinned by checksum and package-lock.json (Letta Code 0.34.25), and git is included because Letta Code keeps agent memory in git. The image is registered in the release's image build and dependabot lists, and install/v1 covers containers and vm hosts.
  • Smoke task: letta-code-smoke is added.

The agent's README.md has the Extension | Implementation table. agent-config, mcp-config, peer-agents, skill-config, trajectory, snapshot, attribution-probe and install are implemented; staging/v1 is the SDK default.

How it was tested

All model calls used openai/gpt-4.1-mini through an OpenAI-compatible endpoint, with Docker 20.10, compose v2 and buildx.

  • agent-env a2a-agent validate --id letta-code --sandbox local: exits 0. The agent card, message/send, tasks/get, agent-config (including the identity and system_prompt round-trips), mcp-config, peer-agents, skill-config, trajectory, install and the changelog roundtrip pass. As in the same run against opencode (also exit 0), snapshot load/recall_after_load report false only because the recall rubric's default judge, claude-code, isn't in a fresh local store. A separate live run saved a conversation, loaded it into another context, and recalled a phrase from before the save.
  • agent-env run agents --task letta-code-smoke: passed, returning 42.
  • make lint and make unit-test: pass. The new tst/unit/agents/letta_code tests cover the bridge's command building, conversation resume and MCP tool exposure.
  • check_plugin_api.py: reports no break to the plugin surface.

RetriggerView in GreptileConfidence Score: 3/5

Fix restore cancellation and finish the outstanding changelog and named-limit changes before merging.

Fix All in CursorFindings

  1. P1 Cancelled restore keeps old reference ▶
  2. P1 Cancelled restore keeps writing ▶
  3. P2 Named timeout is ignored ▶
Fix with agent prompt
### Issue 1
src/agent_env/agents/agents/letta-code/a2a_server.py:466-470
Cancelling `load_snapshot` during the final swap leaves the restored files paired with the old conversation reference. `shielded_thread` waits for the swap to finish, then raises `CancelledError`, so `set_session_ref_for_context` never runs. The next turn resumes the old conversation ID against the restored files, or starts a new conversation if the target was fresh.

Finish updating the reference before allowing cancellation to leave the restore.

### Issue 2
src/agent_env/agents/agents/letta-code/a2a_server.py:undefined-454
`load_snapshot` awaits `_replace_workspace` through `asyncio.to_thread`, but cancelling that await does not stop the thread. Cancellation releases the workspace lock and deletes the temporary staging directory while the thread can still move files. During shutdown or another cancellation, this can race with a new turn or leave the restore incomplete. Wait for the worker to finish before releasing the lock or removing its staging directory. Threaded changelog writes need the same protection.

### Issue 3
src/agent_env/agents/agents/letta-code/letta_code_bridge.py:undefined-309
`_command` hardcodes `120` even though `_CLI_TIMEOUT_S` already defines that limit. This violates the repository requirement to give magic numbers descriptive names rather than use them inline. Use `_CLI_TIMEOUT_S` here, and name the relay size limit and the MCP timeout and buffer limits in `mcp_tools.ts`. This requirement must be satisfied before merging.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds Letta Code as a packaged A2A agent.

  • Letta Code runs tasks with lasting, context-specific conversations.
  • Letta Code snapshots carry conversations and memory between contexts.
  • Tasks can select Letta Code from the shipped agent bundle.

Diagram

sequenceDiagram
  participant Caller
  participant Agent
  participant CLI as Letta Code
  participant Relay
  participant Model
  Caller->>Agent: Send task
  Agent->>CLI: Start or resume conversation
  CLI->>Relay: Model request
  Relay->>Model: Forward with headers and attribution
  Model-->>CLI: Response through relay
  CLI-->>Agent: Stream events and result
  Agent-->>Caller: Task result and conversation reference
Loading

Reviews (7) · Last reviewed commit: "Merge main into add-letta-code-agent" · Reviewed by Greptile

@sarahwooders
sarahwooders requested a review from a team as a code owner October 27, 2026 03:34
@socket-security

socket-security Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​letta-ai/​letta-code@​0.34.268410010099100

View full report

@socket-security

socket-security Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Priority Alert  (click "▶" to expand/collapse) Action
High priority
High CVE: npm sharp: Vulnerabilities in libheif: GHSA-g89c-p67h-r497 and GHSA-2jg2-4ch7-h545

CVE: GHSA-rgj7-g3m4-5g8c sharp: Vulnerabilities in libheif: GHSA-g89c-p67h-r497 and GHSA-2jg2-4ch7-h545 (HIGH)

Affected versions: < 0.35.4

Patched version: 0.35.4

From: src/agent_env/agents/agents/letta-code/package-lock.json → npm/@letta-ai/letta-code@0.34.26 → npm/sharp@0.34.5

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/sharp@0.34.5. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn
High priority
High CVE: npm sharp inherited vulnerabilities in libvips: CVE-2026-33327, CVE-2026-33328, CVE-2026-35590, CVE-2026-35591

CVE: GHSA-f88m-g3jw-g9cj sharp inherited vulnerabilities in libvips: CVE-2026-33327, CVE-2026-33328, CVE-2026-35590, CVE-2026-35591 (HIGH)

Affected versions: < 0.35.0

Patched version: 0.35.0

From: src/agent_env/agents/agents/letta-code/package-lock.json → npm/@letta-ai/letta-code@0.34.26 → npm/sharp@0.34.5

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/sharp@0.34.5. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn

View full report

Comment thread src/agent_env/agents/agents/letta-code/a2a_server.py Outdated
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Cancelled restore keeps writing

load_snapshot awaits _replace_workspace through asyncio.to_thread, but cancelling that await does not stop the thread. Cancellation releases the workspace lock and deletes the temporary staging directory while the thread can still move files. During shutdown or another cancellation, this can race with a new turn or leave the restore incomplete. Wait for the worker to finish before releasing the lock or removing its staging directory. Threaded changelog writes need the same protection.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/agents/agents/letta-code/a2a_server.py
Line: 454

Comment:
**Cancelled restore keeps writing**

`load_snapshot` awaits `_replace_workspace` through `asyncio.to_thread`, but cancelling that await does not stop the thread. Cancellation releases the workspace lock and deletes the temporary staging directory while the thread can still move files. During shutdown or another cancellation, this can race with a new turn or leave the restore incomplete. Wait for the worker to finish before releasing the lock or removing its staging directory. Threaded changelog writes need the same protection.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex

Comment thread src/agent_env/agents/agents/letta-code/Dockerfile Outdated
Comment thread src/agent_env/agents/agents/letta-code/letta_code_bridge.py Outdated
Comment thread src/agent_env/agents/agents/letta-code/letta_code_bridge.py


async def _command(env: dict[str, str], *args: str) -> bytes:
code, out, err = await _communicate("letta", "--backend", "local", *args, env=env, timeout_s=120)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Named timeout is ignored

_command hardcodes 120 even though _CLI_TIMEOUT_S already defines that limit. This violates the repository requirement to give magic numbers descriptive names rather than use them inline. Use _CLI_TIMEOUT_S here, and name the relay size limit and the MCP timeout and buffer limits in mcp_tools.ts. This requirement must be satisfied before merging.

Rule Used: Store magic numbers as class or instance variables with descriptive names rather than using them inline in the code. (source)

Learned From
scaleapi/scaleapi#126388

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/agents/agents/letta-code/letta_code_bridge.py
Line: 309

Comment:
**Named timeout is ignored**

`_command` hardcodes `120` even though `_CLI_TIMEOUT_S` already defines that limit. This violates the repository requirement to give magic numbers descriptive names rather than use them inline. Use `_CLI_TIMEOUT_S` here, and name the relay size limit and the MCP timeout and buffer limits in `mcp_tools.ts`. This requirement must be satisfied before merging.

**Rule Used:** Store magic numbers as class or instance variables with descriptive names rather than using them inline in the code. ([source](https://app.greptile.com/scale-ai/-/custom-context?memory=002e0051-41ad-46c1-9098-47433c580150))

**Learned From**
[scaleapi/scaleapi#126388](https://github.com/scaleapi/scaleapi/pull/126388)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Cursor Fix in Claude Code Fix in Codex

Comment thread src/agent_env/agents/agents/letta-code/letta_code_bridge.py Outdated
Comment thread src/agent_env/config/describe.py Outdated
…llation-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.
Comment thread src/agent_env/agents/agents/letta-code/letta_code_bridge.py Outdated
…leanup 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.
letta-integration Bot and others added 2 commits October 28, 2026 02:51
Comment on lines +466 to +470
if staged is not None:
await shielded_thread(_replace_workspace, staged, commit_session)
else:
await shielded_thread(commit_session)
self.set_session_ref_for_context(target_context_id, imported_session_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Cancelled restore keeps old reference

Cancelling load_snapshot during the final swap leaves the restored files paired with the old conversation reference. shielded_thread waits for the swap to finish, then raises CancelledError, so set_session_ref_for_context never runs. The next turn resumes the old conversation ID against the restored files, or starts a new conversation if the target was fresh.

Finish updating the reference before allowing cancellation to leave the restore.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/agents/agents/letta-code/a2a_server.py
Line: 466-470

Comment:
**Cancelled restore keeps old reference**

Cancelling `load_snapshot` during the final swap leaves the restored files paired with the old conversation reference. `shielded_thread` waits for the swap to finish, then raises `CancelledError`, so `set_session_ref_for_context` never runs. The next turn resumes the old conversation ID against the restored files, or starts a new conversation if the target was fresh.

Finish updating the reference before allowing cancellation to leave the restore.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex

@earakely-scale

Copy link
Copy Markdown
Collaborator

Thank you for the Letta Code agent! It's merged to main through #368 (3051296), which carries this PR's commits unchanged. We opened it from a branch of this repository so that CI could run and we could push review fixes, since this PR doesn't allow edits by maintainers.

Three commits on top of yours:

  • b66a98d: a snapshot load cancelled during its swap still binds the context to the restored conversation. Every file thread that runs under the workspace or changelog lock is shielded, so a cancel can't release the lock while the thread still writes. The MCP tool mod's timeout and output limit are named.
  • 196a183: the per-turn relay answers GET /v1/models with the turn's model alone.
    • Over an endpoint that lists about 3000 models, Letta Code's currentModels spent about 45 s of CPU in agents create, and again in every turn's -p run, merging the list. It scans the whole list for each model.
    • A one-word turn took 50–98 s, and the validator's concurrent prompts timed out at 120 s.
    • With the change, a turn takes about 4 s. This may be worth raising upstream too.
  • 8cb6e00: the shield holds through a repeated cancel, and two id lengths are named.

With those changes, agent-env a2a-agent validate --id letta-code --sandbox local exits 0 on a 4-vCPU Linux VM: every extension is supported and recall_after_load is true, once the claude-code judge is in the store. letta-code-smoke passes, along with all of CI.

Thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants