This repository has been archived on 2026-06-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
nerve/packages/adapter-hermes/src/index.ts
T
xiaoju 4a43a7f3dd refactor: update all consumers to import from @uncaged/workflow
- workflow-utils, workflow-meta: import workflow types from @uncaged/workflow
- adapter-cursor, adapter-hermes: same
- cli: same
- core: remove workflow re-exports, no longer depends on @uncaged/workflow

Phase 5+6 of #320, Testing: #323
2026-05-05 11:01:08 +00:00

126 lines
3.4 KiB
TypeScript

import type { AgentConfig } from "@uncaged/nerve-core";
import { type Result, type SpawnEnv, type SpawnError, ok, spawnSafe } from "@uncaged/nerve-core";
import type { AgentFn, ThreadContext } from "@uncaged/workflow";
/**
* Spawns a non-interactive `hermes chat` invocation with YOLO enabled, argv-only
* (shell: false) following the Nerve issue #208 contract.
*/
export type HermesAgentOptions = {
prompt: string;
model: string | null;
provider: string | null;
skills: string[];
/** When true, suppresses interactive UI noise. */
quiet: boolean;
maxTurns: number;
env: SpawnEnv | null;
timeoutMs: number | null;
dryRun: boolean;
abortSignal: AbortSignal | null;
};
type HermesAgentOptionsInput = HermesAgentOptions | Omit<HermesAgentOptions, "dryRun">;
function resolveHermesDryRun(options: HermesAgentOptionsInput): boolean {
return "dryRun" in options ? options.dryRun : false;
}
function normalizeAbortSignal(options: HermesAgentOptionsInput): AbortSignal | null {
return "abortSignal" in options ? options.abortSignal : null;
}
export async function hermesAgent(
options: HermesAgentOptionsInput,
): Promise<Result<string, SpawnError>> {
const dryRun = resolveHermesDryRun(options);
if (dryRun) {
return ok("[dryRun] hermes stub");
}
const args: string[] = [
"chat",
"-q",
options.prompt,
"--yolo",
"--max-turns",
String(options.maxTurns),
];
if (options.model) {
args.push("--model", options.model);
}
if (options.provider) {
args.push("--provider", options.provider);
}
for (const s of options.skills) {
args.push("-s", s);
}
if (options.quiet) {
args.push("--quiet");
}
const run = await spawnSafe("hermes", args, {
cwd: null,
env: options.env,
timeoutMs: options.timeoutMs,
dryRun: false,
abortSignal: normalizeAbortSignal(options),
});
if (!run.ok) {
return run;
}
return ok(run.value.stdout);
}
function throwHermesSpawnError(error: SpawnError): never {
if (error.kind === "non_zero_exit") {
throw new Error(
`hermes: exitCode=${error.exitCode} stdout=${error.stdout} stderr=${error.stderr}`,
);
}
if (error.kind === "timeout") {
throw new Error("hermes: timeout");
}
if (error.kind === "aborted") {
throw new Error("hermes: aborted");
}
throw new Error(`hermes: ${error.message}`);
}
const HERMES_ADAPTER_DEFAULT_MAX_TURNS = 90;
/** Default wall-clock cap: 300 seconds (milliseconds). */
const HERMES_ADAPTER_DEFAULT_MS = 300_000;
/**
* Builds a Hermes CLI `AgentFn` from adapter config (model, timeout).
*/
export function createHermesAdapter(config: AgentConfig): AgentFn {
const modelFromConfig = config.model === "auto" ? null : config.model;
const timeoutMs = config.timeout;
return async (_ctx: ThreadContext, prompt: string): Promise<string> => {
const run = await hermesAgent({
prompt,
model: modelFromConfig,
provider: null,
skills: [],
quiet: true,
maxTurns: HERMES_ADAPTER_DEFAULT_MAX_TURNS,
env: null,
timeoutMs,
dryRun: false,
abortSignal: null,
});
if (!run.ok) {
throwHermesSpawnError(run.error);
}
return run.value;
};
}
/** Default instance — `model: "auto"`, `timeout: 300` seconds (as milliseconds). */
export const hermesAdapter: AgentFn = createHermesAdapter({
type: "hermes",
model: "auto",
timeout: HERMES_ADAPTER_DEFAULT_MS,
});