fix(agent): defer config validation to call time

Bundle top-level code runs during `workflow add` (descriptor extraction),
but agent config env vars (e.g. WORKFLOW_HERMES_COMMAND) are only available
at `workflow run` time. Deferring validation prevents premature throws.
This commit is contained in:
2026-05-12 12:58:06 +08:00
parent 4c4dabb7a3
commit c5fd84432f
4 changed files with 30 additions and 32 deletions
@@ -101,27 +101,25 @@ describe("createCursorAgent", () => {
expect(typeof agent).toBe("function"); expect(typeof agent).toBe("function");
}); });
test("throws on invalid config at construction", () => { test("defers validation to call time (invalid config does not throw at construction)", () => {
expect(() => const agent = createCursorAgent({
createCursorAgent({ command: "/usr/local/bin/cursor-agent",
command: "/usr/local/bin/cursor-agent", model: null,
model: null, timeout: -1,
timeout: -1, workspace: "/tmp/test-project",
workspace: "/tmp/test-project", llmProvider: null,
llmProvider: null, });
}), expect(typeof agent).toBe("function");
).toThrow();
}); });
test("throws when null workspace without llmProvider", () => { test("defers validation — null workspace without llmProvider does not throw at construction", () => {
expect(() => const agent = createCursorAgent({
createCursorAgent({ command: "/usr/local/bin/cursor-agent",
command: "/usr/local/bin/cursor-agent", model: null,
model: null, timeout: 0,
timeout: 0, workspace: null,
workspace: null, llmProvider: null,
llmProvider: null, });
}), expect(typeof agent).toBe("function");
).toThrow();
}); });
}); });
+5 -5
View File
@@ -30,16 +30,16 @@ function resolveCursorModel(model: string | null): string {
/** Runs `cursor-agent` with workspace from config or extracted from context via LLM. */ /** Runs `cursor-agent` with workspace from config or extracted from context via LLM. */
export function createCursorAgent(config: CursorAgentConfig): AgentFn { export function createCursorAgent(config: CursorAgentConfig): AgentFn {
const validated = validateCursorAgentConfig(config);
if (!validated.ok) {
throw new Error(validated.error);
}
const modelFlag = resolveCursorModel(config.model); const modelFlag = resolveCursorModel(config.model);
const timeoutMs = config.timeout > 0 ? config.timeout : null; const timeoutMs = config.timeout > 0 ? config.timeout : null;
const logger = createLogger({ sink: { kind: "stderr" } }); const logger = createLogger({ sink: { kind: "stderr" } });
return async (ctx) => { return async (ctx) => {
const validated = validateCursorAgentConfig(config);
if (!validated.ok) {
throw new Error(validated.error);
}
let workspace: string; let workspace: string;
if (config.workspace !== null) { if (config.workspace !== null) {
@@ -37,11 +37,11 @@ describe("validateHermesAgentConfig", () => {
}); });
describe("createHermesAgent", () => { describe("createHermesAgent", () => {
test("returns an AgentFn", () => { test("returns an AgentFn even with invalid config (validation deferred to call)", () => {
const agent = createHermesAgent({ const agent = createHermesAgent({
command: "/usr/local/bin/hermes", command: "/usr/local/bin/hermes",
model: null, model: null,
timeout: null, timeout: -5,
}); });
expect(typeof agent).toBe("function"); expect(typeof agent).toBe("function");
}); });
+5 -5
View File
@@ -26,14 +26,14 @@ function throwHermesSpawnError(error: SpawnCliError): never {
/** Runs `hermes chat` non-interactively with the Nerve-style argv contract (`-q`, `--yolo`, `--quiet`). */ /** Runs `hermes chat` non-interactively with the Nerve-style argv contract (`-q`, `--yolo`, `--quiet`). */
export function createHermesAgent(config: HermesAgentConfig): AgentFn { export function createHermesAgent(config: HermesAgentConfig): AgentFn {
const validated = validateHermesAgentConfig(config);
if (!validated.ok) {
throw new Error(validated.error);
}
const timeoutMs = config.timeout; const timeoutMs = config.timeout;
return async (ctx) => { return async (ctx) => {
const validated = validateHermesAgentConfig(config);
if (!validated.ok) {
throw new Error(validated.error);
}
const fullPrompt = await buildAgentPrompt(ctx); const fullPrompt = await buildAgentPrompt(ctx);
const args = [ const args = [
"chat", "chat",