Repository navigation
Conversation
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.
| 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) |
There was a problem hiding this 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.
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.| attachments = [] | ||
| for part in request.parts: | ||
| if isinstance(part, FilePart): | ||
| attachments.extend(await asyncio.to_thread(_materialize_and_render, part, request.task_id)) |
There was a problem hiding this comment.
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.| 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: |
There was a problem hiding this 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.
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.
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 ashermes(alias inaliases.toml) or as@local/agentenv-framework/agents/hermes.hermes agentenv turn, a subcommand that the agent's own Hermes plugin (agentenv_plugin/) adds. It runs the turn in that process, the wayhermes -zdoes, 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.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, formathermes-hook-events/1)./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.$LITELLM_BASE_URL/v1is configured as a named OpenAI-compatible custom provider, keyed fromLITELLM_API_KEY.[model.headers]and the run'sproject_id/task_idattribution go out asextra_headerson every model call, Hermes' auxiliary calls (titles, vision, compression) included. Tag joining matches the OpenCode agent.system_promptis written as the profile'sSOUL.md, the slot Hermes' own identity normally fills. Hermes then sends it as the system prompt.peer_agentsincluded, goes into the profile'smcp_servers, with theAgentEnv-Roleoverride. Installed skills go into one shared folder that every profile reads throughskills.external_dirs.python:3.11-slimwith Hermes installed by its own installer, pinned to thev0.22.5release 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.infra_build.py(image and smoke command),dependabot.yml, the aliases test, the infra-images test, the agents README and CONTRIBUTING. Adds ahermes-smoketask.The agent's
README.mdhas the Extension | Implementation table.agent-config,mcp-config,skill-config(inline and bundle),trajectory,snapshot(save/load),peer-agentsandattribution-probeare implemented;staging/v1is the SDK default. Not included in this PR:install/v1and 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 andanthropic/claude-haiku-5.5as 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 themailenv): every advertised check passed. That covers agent card,message/send/tasks/get, identity round trip, role, system prompt, MCP (search_emailsvia the gateway), trajectory, skill-config (skill_mdandskill_bundle), peer-agents (peer call returned the token), snapshot save + load with recall after load, andtool_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 onlyfalserows 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 scriptedhermesin 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 plainproc.kill().make lintis 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 withHERMES_MAX_CONCURRENT_TURNS.Fix snapshot rollback and cancellation cleanup before merging.
Fix with agent prompt
Summary
Adds Hermes as a bundled A2A agent with per-context profiles, live hook events, skills, MCP tools, and snapshots.
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]Reviews (1) · Last reviewed commit: "feat(agents): add a Hermes Agent agent" · Reviewed by Greptile