4a43a7f3dd
- 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
126 lines
3.4 KiB
TypeScript
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,
|
|
});
|