From 96584e481fc17ef80655962007e5bc5b52097375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 22 May 2026 12:18:14 +0000 Subject: [PATCH] refactor: replace spawnHermes with HermesAcpClient 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 --- packages/workflow-agent-hermes/src/hermes.ts | 123 ++++--------------- packages/workflow-agent-hermes/src/index.ts | 2 +- 2 files changed, 23 insertions(+), 102 deletions(-) diff --git a/packages/workflow-agent-hermes/src/hermes.ts b/packages/workflow-agent-hermes/src/hermes.ts index 5264934..178d49f 100644 --- a/packages/workflow-agent-hermes/src/hermes.ts +++ b/packages/workflow-agent-hermes/src/hermes.ts @@ -1,4 +1,3 @@ -import { spawn } from "node:child_process"; import type { Store } from "@uncaged/json-cas"; import { @@ -8,14 +7,8 @@ import { createAgent, } from "@uncaged/workflow-agent-kit"; -import { - loadHermesSession, - parseSessionIdFromStdout, - storeHermesSessionDetail, -} from "./session-detail.js"; - -const HERMES_COMMAND = "hermes"; -const HERMES_MAX_TURNS = 90; +import { HermesAcpClient } from "./acp-client.js"; +import { storeHermesRawOutput } from "./session-detail.js"; function buildHistorySummary(steps: AgentContext["steps"]): string { if (steps.length === 0) { @@ -52,106 +45,34 @@ export function buildHermesPrompt(ctx: AgentContext): string { return parts.join("\n"); } -function spawnHermes(args: string[]): Promise<{ stdout: string; stderr: string }> { - return new Promise((resolve, reject) => { - const child = spawn(HERMES_COMMAND, args, { - env: process.env, - shell: false, - stdio: ["ignore", "pipe", "pipe"], - }); - - let stdout = ""; - let stderr = ""; - child.stdout?.on("data", (chunk: Buffer) => { - stdout += chunk.toString(); - }); - child.stderr?.on("data", (chunk: Buffer) => { - stderr += chunk.toString(); - }); - - child.on("error", (cause) => { - const message = cause instanceof Error ? cause.message : String(cause); - reject(new Error(`hermes spawn failed: ${message}`)); - }); - - child.on("close", (code) => { - if (code === 0) { - resolve({ stdout, stderr }); - return; - } - const detail = stderr.trim() !== "" ? ` stderr=${stderr.trim()}` : ""; - reject(new Error(`hermes exited with code ${code ?? "null"}${detail}`)); - }); - }); -} - -function spawnHermesChat(prompt: string): Promise<{ stdout: string; stderr: string }> { - return spawnHermes([ - "chat", - "-q", - prompt, - "--yolo", - "--max-turns", - String(HERMES_MAX_TURNS), - "--quiet", - ]); -} - -function spawnHermesResume( - sessionId: string, - message: string, -): Promise<{ stdout: string; stderr: string }> { - return spawnHermes([ - "chat", - "--resume", - sessionId, - "-q", - message, - "--yolo", - "--max-turns", - String(HERMES_MAX_TURNS), - "--quiet", - ]); -} - -function parseSessionId(stdout: string, stderr: string): string { - const sessionId = parseSessionIdFromStdout(stderr) ?? parseSessionIdFromStdout(stdout); - if (sessionId === null) { - throw new Error( - "Failed to parse session_id from hermes output.\n" + - `stderr (first 200 chars): ${stderr.slice(0, 200)}\n` + - `stdout (first 200 chars): ${stdout.slice(0, 200)}`, - ); - } - return sessionId; -} - -async function buildResultFromSession(sessionId: string, store: Store): Promise { - const session = await loadHermesSession(sessionId); - if (session === null) { - throw new Error(`Failed to load hermes session file for session_id: ${sessionId}`); - } - const { detailHash, output } = await storeHermesSessionDetail(store, session); - return { output, detailHash, sessionId }; -} - async function runHermes(ctx: AgentContext): Promise { const fullPrompt = buildHermesPrompt(ctx); - const { stdout, stderr } = await spawnHermesChat(fullPrompt); - const sessionId = parseSessionId(stdout, stderr); - return buildResultFromSession(sessionId, ctx.store); + const client = new HermesAcpClient(); + try { + await client.connect(process.cwd()); + const { text, sessionId } = await client.prompt(fullPrompt); + const detailHash = await storeHermesRawOutput(ctx.store, text); + return { output: text, detailHash, sessionId }; + } finally { + await client.close(); + } } async function continueHermes( - sessionId: string, + _sessionId: string, message: string, store: Store, ): Promise { - const { stdout, stderr } = await spawnHermesResume(sessionId, message); - // Resume may return a new session_id - const newSessionId = parseSessionIdFromStdout(stderr) ?? parseSessionIdFromStdout(stdout); - const resolvedId = newSessionId ?? sessionId; - return buildResultFromSession(resolvedId, store); + // ACP does not support resuming an external session; start a new session with the message as prompt + const client = new HermesAcpClient(); + try { + await client.connect(process.cwd()); + const { text, sessionId } = await client.prompt(message); + const detailHash = await storeHermesRawOutput(store, text); + return { output: text, detailHash, sessionId }; + } finally { + await client.close(); + } } /** Agent CLI factory: parses argv, runs Hermes, extracts output, writes StepNode. */ diff --git a/packages/workflow-agent-hermes/src/index.ts b/packages/workflow-agent-hermes/src/index.ts index 3eb4d4a..cf147d7 100644 --- a/packages/workflow-agent-hermes/src/index.ts +++ b/packages/workflow-agent-hermes/src/index.ts @@ -1,2 +1,2 @@ -export { buildHermesPrompt, createHermesAgent } from "./hermes.js"; export { HermesAcpClient } from "./acp-client.js"; +export { buildHermesPrompt, createHermesAgent } from "./hermes.js";