Skip to content

feat(agents): add a Hermes Agent agent - #384

Open
teknium1 wants to merge 1 commit into
scaleapi:mainfrom
teknium1:feat/hermes-agent
Open

teknium1 wants to merge 1 commit into
scaleapi:mainfrom
teknium1:feat/hermes-agent

Conversation

@teknium1

@teknium1 teknium1 commented Oct 29, 2026 •

Copy link
Copy Markdown

What and why

Adds agents/hermes/, an agent for Nous Research's Hermes Agent, built the same way as the other agents in the bundle. Tasks can name it as hermes (alias in aliases.toml) or as @local/agentenv-framework/agents/hermes.

  • How it runs: each turn is one hermes agentenv turn, a subcommand that the agent's own Hermes plugin (agentenv_plugin/) adds. It runs the turn in that process, the way hermes -z does, instead of handing it to a Hermes gateway. So the turn ends when that process ends, and a timeout or cancel kills the process and every tool it started.
  • Trajectory: the plugin registers Hermes' public plugin hooks (on_session_start, pre_tool_call, post_tool_call, post_api_request, api_request_error, post_llm_call, on_session_end). It writes each event as a JSON line to a pipe the bridge reads while the turn runs (trajectory/v1, live, format hermes-hook-events/1).
  • Per-context isolation: each A2A context is its own Hermes profile (/data/.hermes/profiles/ctx-<hash>), with its own config, sessions database and memory. A context's first turn starts a Hermes session, and later turns resume it with --resume.
  • Model endpoint: $LITELLM_BASE_URL/v1 is configured as a named OpenAI-compatible custom provider, keyed from LITELLM_API_KEY. [model.headers] and the run's project_id/task_id attribution go out as extra_headers on every model call, Hermes' auxiliary calls (titles, vision, compression) included. Tag joining matches the OpenCode agent.
  • System prompt: the task's system_prompt is written as the profile's SOUL.md, the slot Hermes' own identity normally fills. Hermes then sends it as the system prompt.
  • MCP / skills / peers: each registered MCP server, peer_agents included, goes into the profile's mcp_servers, with the AgentEnv-Role override. Installed skills go into one shared folder that every profile reads through skills.external_dirs.
  • Image: python:3.11-slim with Hermes installed by its own installer, pinned to the v0.22.5 release commit. The installer script itself is checked against a SHA-256. Hermes' runtime (its own Python, Node and ripgrep) lives under /data/.hermes, separate from the A2A server's Python. Image size is about 2.9 GB.
  • Release plumbing: added to infra_build.py (image and smoke command), dependabot.yml, the aliases test, the infra-images test, the agents README and CONTRIBUTING. Adds a hermes-smoke task.

The agent's README.md has the Extension | Implementation table. agent-config, mcp-config, skill-config (inline and bundle), trajectory, snapshot (save/load), peer-agents and attribution-probe are implemented; staging/v1 is the SDK default. Not included in this PR: install/v1 and the snapshot changelog. Both can follow if you want them.

Verification

Run locally against the local backends, with the [model] endpoint pointed at an OpenAI-compatible proxy and anthropic/claude-haiku-5.5 as the model:

  • agent-env run agents --task hermes-smoke: passed (deploy 3.8 s, prompt 12.2 s).
  • agent-env a2a-agent put --id hermes-dev --dockerfile .../hermes/Dockerfile (full validator run against the mail env): every advertised check passed. That covers agent card, message/send/tasks/get, identity round trip, role, system prompt, MCP (search_emails via the gateway), trajectory, skill-config (skill_md and skill_bundle), peer-agents (peer call returned the token), snapshot save + load with recall after load, and tool_call_count. It also covers every modality probe: text, png/jpeg/gif, png by https URI, pdf, and also the audio and video probes, which Hermes handled with its own tools. The card declares only text, images and PDF. The only false rows are the two features this PR does not advertise: changelog and install/v1.
  • tst/unit/agents/hermes/: 21 tests. Card contract, message contract (concurrency across contexts, one turn at a time within a context, session binding on cancel, config/MCP/file parts, snapshot round trip), and bridge tests that drive a scripted hermes in a real subprocess: profile config, events read live, failure mapping, output_format, no fd leak. Timeout and cancel both kill Hermes and the tool it started. That last test was confirmed to fail with the process-tree kill swapped for a plain proc.kill().
  • make lint is clean; the rest of the unit suite is left to CI.

A Hermes turn peaked near 210 MB in the image (cgroup memory.peak, running shell and Python tools). The concurrency cap budgets 400 MB per turn past 1 GB, overridable with HERMES_MAX_CONCURRENT_TURNS.

RetriggerView in GreptileConfidence Score: 3/5

Fix snapshot rollback and cancellation cleanup before merging.

Fix All in CursorFindings

  1. P1 Failed loads lose conversation state ▶
  2. P1 Canceled workers keep writing ▶
  3. P2 Attachments bypass the turn limit ▶
Fix with agent prompt
### Issue 1
src/agent_env/agents/agents/hermes/a2a_server.py:452-457
`load_snapshot` replaces the profile before restoring the workspace, but updates the session reference afterward. If moving the workspace fails-for example, while copying onto a full workspace volume-`_replace_workspace` restores the old files, but the old profile is already gone. The next turn uses the old session reference against the restored conversation database.

Keep the old profile until both replacements succeed, and roll back both on failure.

### Issue 2
src/agent_env/agents/agents/hermes/a2a_server.py:337
Canceling the task awaiting `asyncio.to_thread(_materialize_and_render, ...)` releases `_workspace.shared()`, but does not stop the worker. The download or `pdftoppm` process can keep writing while a snapshot holds the supposedly exclusive workspace lock. The snapshot handlers have the same problem with their threaded file changes.

Wait for started work to stop before releasing the locks. Cancel and reap rendering processes explicitly.

### Issue 3
src/agent_env/agents/agents/hermes/a2a_server.py:333-341
Attachment downloads and PDF rendering run before `_turn_slots` is acquired. Tasks in different contexts can therefore start many downloads and `pdftoppm` processes even with `HERMES_MAX_CONCURRENT_TURNS=1`. Downloads also hold the entire response in memory, so file-heavy tasks can exhaust memory despite the configured limit.

Move attachment work inside the same concurrency limit.

---

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

Summary

Adds Hermes as a bundled A2A agent with per-context profiles, live hook events, skills, MCP tools, and snapshots.

  • Hermes tasks run in separate, resumable context sessions.
  • Saved Hermes conversations can return with their workspace.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[A2A task] --> B[Context lock and shared workspace lock]
  B --> C[Download attachments and render PDFs]
  C --> D[Take turn slot]
  D --> E[Write context profile]
  E --> F[Run Hermes process]
  F --> G[Read live hook events]
  G --> H[Return answer and usage]
  S[Snapshot request] --> X[Exclusive workspace lock]
  X --> Y[Save or restore profile and workspace]
Loading

Reviews (1) · Last reviewed commit: "feat(agents): add a Hermes Agent agent" · Reviewed by Greptile

Adds agents/hermes/, Nous Research's Hermes Agent behind the A2A agent framework: each turn is one in-process `hermes agentenv turn` (a subcommand of the agent's own Hermes plugin) in the context's own Hermes profile, its plugin hook events streamed as the trajectory. Registers the image, its alias, a smoke task, and the release/dependabot entries.
@teknium1
teknium1 requested a review from a team as a code owner October 29, 2026 05:24
Comment on lines +452 to +457
session_id = await asyncio.to_thread(_restore_profile, conversation, home)
except (tarfile.TarError, OSError, ValueError) as exc:
raise HTTPException(status_code=400, detail=f"Unreadable Hermes snapshot: {exc}") from exc
if staged is not None:
await asyncio.to_thread(_replace_workspace, staged)
self.set_session_ref_for_context(target_context_id, 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 Failed loads lose conversation state

load_snapshot replaces the profile before restoring the workspace, but updates the session reference afterward. If moving the workspace fails—for example, while copying onto a full workspace volume—_replace_workspace restores the old files, but the old profile is already gone. The next turn uses the old session reference against the restored conversation database.

Keep the old profile until both replacements succeed, and roll back both on failure.

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

Comment:
**Failed loads lose conversation state**

`load_snapshot` replaces the profile before restoring the workspace, but updates the session reference afterward. If moving the workspace fails—for example, while copying onto a full workspace volume—`_replace_workspace` restores the old files, but the old profile is already gone. The next turn uses the old session reference against the restored conversation database.

Keep the old profile until both replacements succeed, and roll back both on failure.

---

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

attachments = []
for part in request.parts:
if isinstance(part, FilePart):
attachments.extend(await asyncio.to_thread(_materialize_and_render, part, request.task_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 Canceled workers keep writing

Canceling the task awaiting asyncio.to_thread(_materialize_and_render, ...) releases _workspace.shared(), but does not stop the worker. The download or pdftoppm process can keep writing while a snapshot holds the supposedly exclusive workspace lock. The snapshot handlers have the same problem with their threaded file changes.

Wait for started work to stop before releasing the locks. Cancel and reap rendering processes explicitly.

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

Comment:
**Canceled workers keep writing**

Canceling the task awaiting `asyncio.to_thread(_materialize_and_render, ...)` releases `_workspace.shared()`, but does not stop the worker. The download or `pdftoppm` process can keep writing while a snapshot holds the supposedly exclusive workspace lock. The snapshot handlers have the same problem with their threaded file changes.

Wait for started work to stop before releasing the locks. Cancel and reap rendering processes explicitly.

---

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 on lines +333 to +341
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:
async with self._turn_slots:

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 Attachments bypass the turn limit

Attachment downloads and PDF rendering run before _turn_slots is acquired. Tasks in different contexts can therefore start many downloads and pdftoppm processes even with HERMES_MAX_CONCURRENT_TURNS=1. Downloads also hold the entire response in memory, so file-heavy tasks can exhaust memory despite the configured limit.

Move attachment work inside the same concurrency limit.

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

Comment:
**Attachments bypass the turn limit**

Attachment downloads and PDF rendering run before `_turn_slots` is acquired. Tasks in different contexts can therefore start many downloads and `pdftoppm` processes even with `HERMES_MAX_CONCURRENT_TURNS=1`. Downloads also hold the entire response in memory, so file-heavy tasks can exhaust memory despite the configured limit.

Move attachment work inside the same concurrency limit.

---

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

This branch has not been deployed

No deployments
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.

1 participant