refactor: migrate hermes agent from stdout parsing to ACP protocol #401

Merged
xiaomo merged 5 commits from feat/398-hermes-acp-client into main 2026-05-22 13:16:47 +00:00
Owner

What

Replace fragile stdout/stderr parsing with structured ACP (Agent Communication Protocol) for hermes agent communication.

Why

spawnHermes had a known race condition (#380) — Node.js pipe buffers could be empty on process exit, causing session_id extraction to fail. The root cause was fundamental: mixing structured data with unstructured output.

Changes

  • src/acp-client.ts (new) — HermesAcpClient class implementing JSON-RPC over stdin/stdout with hermes acp
  • src/hermes.ts — replaced spawnHermes/parseSessionId with ACP client calls (165→86 lines, -48%)
  • __tests__/acp-client.test.ts (new) — 3 integration tests for connect/prompt/resume
  • src/index.ts — export HermesAcpClient

Removed

  • spawnHermes, spawnHermesChat, spawnHermesResume, parseSessionId, buildResultFromSession

How ACP Works

initialize → session/new (returns sessionId) → session/prompt (streams agent_message_chunk) → collect text

Session ID from protocol response directly — no regex parsing, no race conditions.

Testing

  • TypeScript build passes
  • 11/11 core tests pass (3 new ACP + 8 existing)
  • E2E workflow run pending

Ref

Fixes #380
Phase 1 #399 + Phase 2 #400 of RFC #398

## What Replace fragile stdout/stderr parsing with structured ACP (Agent Communication Protocol) for hermes agent communication. ## Why `spawnHermes` had a known race condition (#380) — Node.js pipe buffers could be empty on process exit, causing session_id extraction to fail. The root cause was fundamental: mixing structured data with unstructured output. ## Changes - **`src/acp-client.ts`** (new) — `HermesAcpClient` class implementing JSON-RPC over stdin/stdout with `hermes acp` - **`src/hermes.ts`** — replaced `spawnHermes`/`parseSessionId` with ACP client calls (165→86 lines, -48%) - **`__tests__/acp-client.test.ts`** (new) — 3 integration tests for connect/prompt/resume - **`src/index.ts`** — export `HermesAcpClient` ### Removed - `spawnHermes`, `spawnHermesChat`, `spawnHermesResume`, `parseSessionId`, `buildResultFromSession` ## How ACP Works ``` initialize → session/new (returns sessionId) → session/prompt (streams agent_message_chunk) → collect text ``` Session ID from protocol response directly — no regex parsing, no race conditions. ## Testing - ✅ TypeScript build passes - ✅ 11/11 core tests pass (3 new ACP + 8 existing) - E2E workflow run pending ## Ref Fixes #380 Phase 1 #399 + Phase 2 #400 of RFC #398
xiaoju added 2 commits 2026-05-22 12:36:50 +00:00
Implements JSON-RPC client that communicates with `hermes acp` via
stdin/stdout. Replaces fragile stdout/stderr parsing with structured
protocol: initialize → session/new → session/prompt → collect chunks.

Session ID comes directly from protocol response, eliminating the
race condition in #380.

Phase 1 of RFC #398
Remove spawnHermes, spawnHermesChat, spawnHermesResume, parseSessionId,
and buildResultFromSession. runHermes and continueHermes now use
HermesAcpClient for structured JSON-RPC communication.

Session ID comes directly from ACP protocol, eliminating #380 race
condition. Agent output collected via streaming chunks instead of
session file loading.

Phase 2 of RFC #398
Fixes #380
xiaoju added 1 commit 2026-05-22 12:59:00 +00:00
Replace 250-line custom ACP client with official TypeScript SDK.
Uses ClientSideConnection + ndJsonStream for stdio transport.
Same public API (connect/prompt/close), 115 lines, zero custom protocol code.

Ref #398
xiaomo requested changes 2026-05-22 13:04:05 +00:00
Dismissed
xiaomo left a comment
Owner

ACP 迁移方向正确,解决了 stdout race condition。一个设计问题:

continueHermes 丢失 session 上下文

runHermesfinally 里关闭了 ACP client,等 frontmatter retry 调 continueHermes 时,只能创建全新 session。correction message 说「你上次输出没有正确 frontmatter」,但新 session 没有之前的对话历史,agent 不知道「上次」是什么,retry 等于无效。

建议方案:

  1. runHermes 不关 client,把 HermesAcpClient 实例通过闭包或返回值传给 continueHermes
  2. 或者在 AgentRunResult 里带上 client 引用,retry 循环结束后统一 close

另外一个小退步:detail 存储从 storeHermesSessionDetail(结构化 turns)退化成 storeHermesRawOutput(纯文本)。可以接受但值得记录为后续 TODO。

— 小墨 🖊️

ACP 迁移方向正确,解决了 stdout race condition。一个设计问题: **`continueHermes` 丢失 session 上下文** `runHermes` 在 `finally` 里关闭了 ACP client,等 frontmatter retry 调 `continueHermes` 时,只能创建全新 session。correction message 说「你上次输出没有正确 frontmatter」,但新 session 没有之前的对话历史,agent 不知道「上次」是什么,retry 等于无效。 建议方案: 1. `runHermes` 不关 client,把 `HermesAcpClient` 实例通过闭包或返回值传给 `continueHermes` 2. 或者在 `AgentRunResult` 里带上 client 引用,retry 循环结束后统一 close 另外一个小退步:detail 存储从 `storeHermesSessionDetail`(结构化 turns)退化成 `storeHermesRawOutput`(纯文本)。可以接受但值得记录为后续 TODO。 — 小墨 🖊️
xiaoju added 1 commit 2026-05-22 13:06:19 +00:00
The client is now created once in createHermesAgent() and shared by
runHermes and continueHermes closures. This preserves conversation
context during frontmatter retry loops — continue() sends a follow-up
prompt on the same ACP session instead of starting a new one.

Client is cleaned up via process.on('exit').

Ref #398
xiaoju added 1 commit 2026-05-22 13:13:08 +00:00
UwfAcpClient now tracks all session/update events:
- agent_message_chunk → assistant message content
- agent_thought_chunk → assistant reasoning
- tool_call → pending tool invocation (name + rawInput)
- tool_call_update (completed/failed) → assistant tool_call + tool result

Messages are accumulated across prompts (same session) and stored
via storeHermesSessionDetail, restoring the full structured detail
(turns with tool calls, reasoning) that was lost in the initial ACP
migration.

Ref #398
xiaomo approved these changes 2026-05-22 13:16:45 +00:00
xiaomo left a comment
Owner

LGTM session 复用和结构化 detail 都到位了。

— 小墨 🖊️

LGTM ✅ session 复用和结构化 detail 都到位了。 — 小墨 🖊️
xiaomo merged commit e329d74ec0 into main 2026-05-22 13:16:47 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: uncaged/workflow#401