Refactor workflows to use compileWorkflowSpec from nerve-daemon

Remove workflows/_shared; wire createLlmExtractFn, zodMeta, and createCursorAdapter(mode ask). Plan/implement/publish compile inner specs via daemon.

Made-with: Cursor
This commit is contained in:
小橘 🍊(NEKO Team) 2026-04-29 09:35:13 +00:00 committed by 小橘
parent 56ce22fb1b
commit 70fd064bad
12 changed files with 327 additions and 270 deletions

9
pnpm-lock.yaml generated
View File

@ -112,6 +112,9 @@ importers:
'@uncaged/nerve-core': '@uncaged/nerve-core':
specifier: link:../../../repos/nerve/packages/core specifier: link:../../../repos/nerve/packages/core
version: link:../../../repos/nerve/packages/core version: link:../../../repos/nerve/packages/core
'@uncaged/nerve-daemon':
specifier: link:../../../repos/nerve/packages/daemon
version: link:../../../repos/nerve/packages/daemon
'@uncaged/nerve-workflow-utils': '@uncaged/nerve-workflow-utils':
specifier: link:../../../repos/nerve/packages/workflow-utils specifier: link:../../../repos/nerve/packages/workflow-utils
version: link:../../../repos/nerve/packages/workflow-utils version: link:../../../repos/nerve/packages/workflow-utils
@ -140,6 +143,9 @@ importers:
'@uncaged/nerve-core': '@uncaged/nerve-core':
specifier: link:../../../repos/nerve/packages/core specifier: link:../../../repos/nerve/packages/core
version: link:../../../repos/nerve/packages/core version: link:../../../repos/nerve/packages/core
'@uncaged/nerve-daemon':
specifier: link:../../../repos/nerve/packages/daemon
version: link:../../../repos/nerve/packages/daemon
'@uncaged/nerve-workflow-utils': '@uncaged/nerve-workflow-utils':
specifier: link:../../../repos/nerve/packages/workflow-utils specifier: link:../../../repos/nerve/packages/workflow-utils
version: link:../../../repos/nerve/packages/workflow-utils version: link:../../../repos/nerve/packages/workflow-utils
@ -168,6 +174,9 @@ importers:
'@uncaged/nerve-core': '@uncaged/nerve-core':
specifier: link:../../../repos/nerve/packages/core specifier: link:../../../repos/nerve/packages/core
version: link:../../../repos/nerve/packages/core version: link:../../../repos/nerve/packages/core
'@uncaged/nerve-daemon':
specifier: link:../../../repos/nerve/packages/daemon
version: link:../../../repos/nerve/packages/daemon
'@uncaged/nerve-workflow-utils': '@uncaged/nerve-workflow-utils':
specifier: link:../../../repos/nerve/packages/workflow-utils specifier: link:../../../repos/nerve/packages/workflow-utils
version: link:../../../repos/nerve/packages/workflow-utils version: link:../../../repos/nerve/packages/workflow-utils

View File

@ -1,46 +0,0 @@
import { cursorAgent } from "@uncaged/nerve-adapter-cursor";
import type { AgentFn, WorkflowContext } from "@uncaged/nerve-core";
const DEFAULT_MS = 300_000;
function throwCursorError(error: {
kind: string;
exitCode?: number;
stdout?: string;
stderr?: string;
message?: string;
}): never {
if (error.kind === "non_zero_exit") {
throw new Error(
`cursor-agent: exitCode=${error.exitCode} stdout=${error.stdout} stderr=${error.stderr}`,
);
}
if (error.kind === "timeout") {
throw new Error("cursor-agent: timeout");
}
if (error.kind === "aborted") {
throw new Error("cursor-agent: aborted");
}
throw new Error(`cursor-agent: ${error.message ?? error.kind}`);
}
/** Cursor CLI in `--mode=ask` (planner-style roles). */
export function createAskCursorAdapter(config: { model: string; timeout: number | null }): AgentFn {
const timeoutMs = config.timeout ?? DEFAULT_MS;
return async (prompt: string, context: WorkflowContext): Promise<string> => {
const run = await cursorAgent({
prompt,
mode: "ask",
model: config.model,
cwd: context.workdir,
env: null,
timeoutMs,
dryRun: context.start.meta.dryRun,
abortSignal: context.signal,
});
if (!run.ok) {
throwCursorError(run.error);
}
return run.value;
};
}

View File

@ -1,52 +0,0 @@
import type {
Role,
RoleSpec,
StartStep,
WorkflowContext,
WorkflowMessage,
} from "@uncaged/nerve-core";
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { extractMetaOrThrow, type ZodMetaSchema } from "@uncaged/nerve-workflow-utils";
import type { z } from "zod";
export function zodMeta<T>(zod: z.ZodType<T>): ZodMetaSchema<T> {
return { witness: null, zod };
}
export type CompileRoleSpecDeps = {
provider: LlmProvider;
createContext: (start: StartStep, messages: WorkflowMessage[]) => WorkflowContext;
};
/**
* RFC-003 RoleSpec runtime Role (extract uses global nerve.yaml provider via `provider`;
* dryRun follows each invocation's start frame).
*/
export function compileRoleSpec<M extends Record<string, unknown>>(
spec: RoleSpec<M>,
deps: CompileRoleSpecDeps,
): Role<M> {
return async (start, messages) => {
const ctx = deps.createContext(start, messages);
const promptText =
typeof spec.prompt === "string" ? spec.prompt : await spec.prompt(start, messages);
const raw = await spec.adapter(promptText, ctx);
const zod = (spec.meta as ZodMetaSchema<M>).zod;
const meta = await extractMetaOrThrow(raw, zod, {
provider: deps.provider,
dryRun: start.meta.dryRun,
});
return { content: raw, meta };
};
}
export function defaultAgentCreateContext(
workdir: string,
): (start: StartStep, messages: WorkflowMessage[]) => WorkflowContext {
return (start, messages) => ({
start,
messages,
workdir,
signal: new AbortController().signal,
});
}

View File

@ -1,10 +1,17 @@
import type { StartStep, WorkflowDefinition } from "@uncaged/nerve-core"; import type {
Schema,
StartStep,
WorkflowContext,
WorkflowDefinition,
WorkflowMessage,
WorkflowSpec,
} from "@uncaged/nerve-core";
import { END } from "@uncaged/nerve-core";
import { compileWorkflowSpec, type CompileWorkflowSpecDeps } from "@uncaged/nerve-daemon";
import { createCursorAdapter } from "@uncaged/nerve-adapter-cursor"; import { createCursorAdapter } from "@uncaged/nerve-adapter-cursor";
import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import { hermesAdapter } from "@uncaged/nerve-adapter-hermes";
import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils";
import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../_shared/rfc003-compile.js";
import { createAskCursorAdapter } from "../_shared/cursor-ask-adapter.js";
import { coderPrompt } from "./roles/coder/prompt.js"; import { coderPrompt } from "./roles/coder/prompt.js";
import { coderMetaSchema } from "./roles/coder/index.js"; import { coderMetaSchema } from "./roles/coder/index.js";
@ -25,16 +32,39 @@ export type BuildSenseGeneratorDeps = {
const CURSOR_TIMEOUT_MS = 300_000; const CURSOR_TIMEOUT_MS = 300_000;
type SenseAgentMeta = Pick<SenseMeta, "planner" | "coder" | "reviewer" | "tester">;
function defaultAgentCreateContext(
workdir: string,
): (start: StartStep, messages: WorkflowMessage[]) => WorkflowContext {
return (start, messages) => ({
start,
messages,
workdir,
signal: new AbortController().signal,
});
}
export function buildSenseGenerator({ export function buildSenseGenerator({
provider, provider,
cwd, cwd,
}: BuildSenseGeneratorDeps): WorkflowDefinition<SenseMeta> { }: BuildSenseGeneratorDeps): WorkflowDefinition<SenseMeta> {
const createContext = defaultAgentCreateContext(cwd); const compileDeps: CompileWorkflowSpecDeps = {
const deps = { provider, createContext }; extractFn: async <T>(raw: string, schema: Schema<T>, dryRun: boolean) =>
createLlmExtractFn<T>({ provider, dryRun })(raw, schema),
createContext: defaultAgentCreateContext(cwd),
};
const agentRoles = { const agentSpec: WorkflowSpec<SenseAgentMeta> = {
name: "develop-sense",
roles: {
planner: { planner: {
adapter: createAskCursorAdapter({ model: "auto", timeout: CURSOR_TIMEOUT_MS }), adapter: createCursorAdapter({
type: "cursor",
model: "auto",
timeout: CURSOR_TIMEOUT_MS,
mode: "ask",
}),
prompt: async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }), prompt: async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }),
meta: zodMeta(plannerMetaSchema), meta: zodMeta(plannerMetaSchema),
}, },
@ -59,15 +89,19 @@ export function buildSenseGenerator({
testerPrompt({ threadId: start.meta.threadId, nerveRoot: cwd }), testerPrompt({ threadId: start.meta.threadId, nerveRoot: cwd }),
meta: zodMeta(testerMetaSchema), meta: zodMeta(testerMetaSchema),
}, },
},
moderator: () => END,
}; };
const compiled = compileWorkflowSpec(agentSpec, compileDeps);
return { return {
name: "develop-sense", name: "develop-sense",
roles: { roles: {
planner: compileRoleSpec(agentRoles.planner, deps), planner: compiled.roles.planner,
coder: compileRoleSpec(agentRoles.coder, deps), coder: compiled.roles.coder,
reviewer: compileRoleSpec(agentRoles.reviewer, deps), reviewer: compiled.roles.reviewer,
tester: compileRoleSpec(agentRoles.tester, deps), tester: compiled.roles.tester,
committer: buildCommitterRole({ nerveRoot: cwd }), committer: buildCommitterRole({ nerveRoot: cwd }),
}, },
moderator, moderator,

View File

@ -10,6 +10,7 @@
"@uncaged/nerve-adapter-cursor": "latest", "@uncaged/nerve-adapter-cursor": "latest",
"@uncaged/nerve-adapter-hermes": "latest", "@uncaged/nerve-adapter-hermes": "latest",
"@uncaged/nerve-core": "latest", "@uncaged/nerve-core": "latest",
"@uncaged/nerve-daemon": "latest",
"@uncaged/nerve-workflow-utils": "latest", "@uncaged/nerve-workflow-utils": "latest",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },

View File

@ -1,10 +1,17 @@
import type { StartStep, WorkflowDefinition } from "@uncaged/nerve-core"; import type {
Schema,
StartStep,
WorkflowContext,
WorkflowDefinition,
WorkflowMessage,
WorkflowSpec,
} from "@uncaged/nerve-core";
import { END } from "@uncaged/nerve-core";
import { compileWorkflowSpec, type CompileWorkflowSpecDeps } from "@uncaged/nerve-daemon";
import { createCursorAdapter } from "@uncaged/nerve-adapter-cursor"; import { createCursorAdapter } from "@uncaged/nerve-adapter-cursor";
import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import { hermesAdapter } from "@uncaged/nerve-adapter-hermes";
import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils";
import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../_shared/rfc003-compile.js";
import { createAskCursorAdapter } from "../_shared/cursor-ask-adapter.js";
import { coderPrompt } from "./roles/coder/prompt.js"; import { coderPrompt } from "./roles/coder/prompt.js";
import { coderMetaSchema } from "./roles/coder/index.js"; import { coderMetaSchema } from "./roles/coder/index.js";
@ -25,16 +32,39 @@ export type BuildWorkflowGeneratorDeps = {
const CURSOR_TIMEOUT_MS = 300_000; const CURSOR_TIMEOUT_MS = 300_000;
type WorkflowAgentMeta = Pick<WorkflowMeta, "planner" | "coder" | "reviewer" | "tester">;
function defaultAgentCreateContext(
workdir: string,
): (start: StartStep, messages: WorkflowMessage[]) => WorkflowContext {
return (start, messages) => ({
start,
messages,
workdir,
signal: new AbortController().signal,
});
}
export function buildWorkflowGenerator({ export function buildWorkflowGenerator({
provider, provider,
nerveRoot, nerveRoot,
}: BuildWorkflowGeneratorDeps): WorkflowDefinition<WorkflowMeta> { }: BuildWorkflowGeneratorDeps): WorkflowDefinition<WorkflowMeta> {
const createContext = defaultAgentCreateContext(nerveRoot); const compileDeps: CompileWorkflowSpecDeps = {
const deps = { provider, createContext }; extractFn: async <T>(raw: string, schema: Schema<T>, dryRun: boolean) =>
createLlmExtractFn<T>({ provider, dryRun })(raw, schema),
createContext: defaultAgentCreateContext(nerveRoot),
};
const agentRoles = { const agentSpec: WorkflowSpec<WorkflowAgentMeta> = {
name: "develop-workflow",
roles: {
planner: { planner: {
adapter: createAskCursorAdapter({ model: "auto", timeout: CURSOR_TIMEOUT_MS }), adapter: createCursorAdapter({
type: "cursor",
model: "auto",
timeout: CURSOR_TIMEOUT_MS,
mode: "ask",
}),
prompt: async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }), prompt: async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }),
meta: zodMeta(plannerMetaSchema), meta: zodMeta(plannerMetaSchema),
}, },
@ -59,15 +89,19 @@ export function buildWorkflowGenerator({
testerPrompt({ threadId: start.meta.threadId, nerveRoot }), testerPrompt({ threadId: start.meta.threadId, nerveRoot }),
meta: zodMeta(testerMetaSchema), meta: zodMeta(testerMetaSchema),
}, },
},
moderator: () => END,
}; };
const compiled = compileWorkflowSpec(agentSpec, compileDeps);
return { return {
name: "develop-workflow", name: "develop-workflow",
roles: { roles: {
planner: compileRoleSpec(agentRoles.planner, deps), planner: compiled.roles.planner,
coder: compileRoleSpec(agentRoles.coder, deps), coder: compiled.roles.coder,
reviewer: compileRoleSpec(agentRoles.reviewer, deps), reviewer: compiled.roles.reviewer,
tester: compileRoleSpec(agentRoles.tester, deps), tester: compiled.roles.tester,
committer: buildCommitterRole({ nerveRoot }), committer: buildCommitterRole({ nerveRoot }),
}, },
moderator, moderator,

View File

@ -10,6 +10,7 @@
"@uncaged/nerve-adapter-cursor": "latest", "@uncaged/nerve-adapter-cursor": "latest",
"@uncaged/nerve-adapter-hermes": "latest", "@uncaged/nerve-adapter-hermes": "latest",
"@uncaged/nerve-core": "latest", "@uncaged/nerve-core": "latest",
"@uncaged/nerve-daemon": "latest",
"@uncaged/nerve-workflow-utils": "latest", "@uncaged/nerve-workflow-utils": "latest",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },

View File

@ -1,8 +1,16 @@
import type { StartStep, WorkflowDefinition } from "@uncaged/nerve-core"; import type {
Schema,
StartStep,
WorkflowContext,
WorkflowDefinition,
WorkflowMessage,
WorkflowSpec,
} from "@uncaged/nerve-core";
import { END } from "@uncaged/nerve-core";
import { compileWorkflowSpec, type CompileWorkflowSpecDeps } from "@uncaged/nerve-daemon";
import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import { hermesAdapter } from "@uncaged/nerve-adapter-hermes";
import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils";
import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../_shared/rfc003-compile.js";
import { moderator } from "./moderator.js"; import { moderator } from "./moderator.js";
import type { WorkflowMeta } from "./moderator.js"; import type { WorkflowMeta } from "./moderator.js";
@ -23,11 +31,29 @@ export type BuildSolveIssueDeps = {
provider: LlmProvider; provider: LlmProvider;
}; };
export function buildSolveIssue({ nerveRoot, provider }: BuildSolveIssueDeps): WorkflowDefinition<WorkflowMeta> { type SolveHermesAgentMeta = Pick<WorkflowMeta, "read-issue" | "prepare" | "review" | "test">;
const createContext = defaultAgentCreateContext(nerveRoot);
const deps = { provider, createContext };
const agentRoles = { function defaultAgentCreateContext(
workdir: string,
): (start: StartStep, messages: WorkflowMessage[]) => WorkflowContext {
return (start, messages) => ({
start,
messages,
workdir,
signal: new AbortController().signal,
});
}
export function buildSolveIssue({ nerveRoot, provider }: BuildSolveIssueDeps): WorkflowDefinition<WorkflowMeta> {
const compileDeps: CompileWorkflowSpecDeps = {
extractFn: async <T>(raw: string, schema: Schema<T>, dryRun: boolean) =>
createLlmExtractFn<T>({ provider, dryRun })(raw, schema),
createContext: defaultAgentCreateContext(nerveRoot),
};
const agentSpec: WorkflowSpec<SolveHermesAgentMeta> = {
name: "solve-issue",
roles: {
"read-issue": { "read-issue": {
adapter: hermesAdapter, adapter: hermesAdapter,
prompt: async (start: StartStep) => readIssuePrompt({ threadId: start.meta.threadId }), prompt: async (start: StartStep) => readIssuePrompt({ threadId: start.meta.threadId }),
@ -49,17 +75,21 @@ export function buildSolveIssue({ nerveRoot, provider }: BuildSolveIssueDeps): W
prompt: async (start: StartStep) => testPrompt({ threadId: start.meta.threadId }), prompt: async (start: StartStep) => testPrompt({ threadId: start.meta.threadId }),
meta: zodMeta(testMetaSchema), meta: zodMeta(testMetaSchema),
}, },
},
moderator: () => END,
}; };
const compiled = compileWorkflowSpec(agentSpec, compileDeps);
return { return {
name: "solve-issue", name: "solve-issue",
roles: { roles: {
"read-issue": compileRoleSpec(agentRoles["read-issue"], deps), "read-issue": compiled.roles["read-issue"],
prepare: compileRoleSpec(agentRoles.prepare, deps), prepare: compiled.roles.prepare,
plan: buildPlanRole({ provider, nerveRoot }), plan: buildPlanRole({ provider, nerveRoot }),
implement: buildImplementRole({ provider, nerveRoot }), implement: buildImplementRole({ provider, nerveRoot }),
review: compileRoleSpec(agentRoles.review, deps), review: compiled.roles.review,
test: compileRoleSpec(agentRoles.test, deps), test: compiled.roles.test,
publish: buildPublishRole({ provider, nerveRoot }), publish: buildPublishRole({ provider, nerveRoot }),
}, },
moderator, moderator,

View File

@ -10,6 +10,7 @@
"@uncaged/nerve-adapter-cursor": "latest", "@uncaged/nerve-adapter-cursor": "latest",
"@uncaged/nerve-adapter-hermes": "latest", "@uncaged/nerve-adapter-hermes": "latest",
"@uncaged/nerve-core": "latest", "@uncaged/nerve-core": "latest",
"@uncaged/nerve-daemon": "latest",
"@uncaged/nerve-workflow-utils": "latest", "@uncaged/nerve-workflow-utils": "latest",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },

View File

@ -1,10 +1,11 @@
import type { Role, RoleResult, RoleSpec, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; import type { Role, RoleResult, Schema, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
import { END } from "@uncaged/nerve-core";
import { compileWorkflowSpec } from "@uncaged/nerve-daemon";
import { createCursorAdapter } from "@uncaged/nerve-adapter-cursor"; import { createCursorAdapter } from "@uncaged/nerve-adapter-cursor";
import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils";
import { z } from "zod"; import { z } from "zod";
import { compileRoleSpec, zodMeta } from "../../../_shared/rfc003-compile.js";
import { resolveRepoCwd } from "../../lib/repo-context.js"; import { resolveRepoCwd } from "../../lib/repo-context.js";
import { buildImplementPrompt } from "./prompt.js"; import { buildImplementPrompt } from "./prompt.js";
@ -21,7 +22,17 @@ export type BuildImplementDeps = {
const CURSOR_TIMEOUT_MS = 300_000; const CURSOR_TIMEOUT_MS = 300_000;
export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps): Role<ImplementMeta> { export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps): Role<ImplementMeta> {
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<ImplementMeta>> => {
const cwd = resolveRepoCwd(messages);
if (cwd === null) {
return {
content: "implement cannot run: missing repo path in thread markers",
meta: { done: false },
};
}
const innerSpec = { const innerSpec = {
main: {
adapter: createCursorAdapter({ adapter: createCursorAdapter({
type: "cursor", type: "cursor",
model: "auto", model: "auto",
@ -33,29 +44,29 @@ export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps):
nerveRoot, nerveRoot,
}), }),
meta: zodMeta(implementMetaSchema), meta: zodMeta(implementMetaSchema),
} satisfies RoleSpec<ImplementMeta>; },
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<ImplementMeta>> => {
const cwd = resolveRepoCwd(messages);
if (cwd === null) {
return {
content: "implement cannot run: missing repo path in thread markers",
meta: { done: false },
}; };
}
const run = compileRoleSpec(innerSpec, { const compiled = compileWorkflowSpec(
provider, {
name: "_implement-inner",
roles: innerSpec,
moderator: () => END,
},
{
extractFn: async <T>(raw: string, schema: Schema<T>, dryRun: boolean) =>
createLlmExtractFn<T>({ provider, dryRun })(raw, schema),
createContext: (s, m) => ({ createContext: (s, m) => ({
start: s, start: s,
messages: m, messages: m,
workdir: cwd, workdir: cwd,
signal: new AbortController().signal, signal: new AbortController().signal,
}), }),
}); },
);
try { try {
return await run(start, messages); return await compiled.roles.main(start, messages);
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e); const msg = e instanceof Error ? e.message : String(e);
return { return {

View File

@ -1,7 +1,9 @@
import type { Role, RoleResult, RoleSpec, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; import type { Role, RoleResult, Schema, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
import { createAskCursorAdapter } from "../../../_shared/cursor-ask-adapter.js"; import { END } from "@uncaged/nerve-core";
import { compileRoleSpec, zodMeta } from "../../../_shared/rfc003-compile.js"; import { compileWorkflowSpec } from "@uncaged/nerve-daemon";
import { createCursorAdapter } from "@uncaged/nerve-adapter-cursor";
import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils";
import { z } from "zod"; import { z } from "zod";
import { resolveRepoCwd } from "../../lib/repo-context.js"; import { resolveRepoCwd } from "../../lib/repo-context.js";
@ -20,16 +22,6 @@ export type BuildPlanDeps = {
const CURSOR_TIMEOUT_MS = 300_000; const CURSOR_TIMEOUT_MS = 300_000;
export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role<PlanMeta> { export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role<PlanMeta> {
const innerSpec = {
adapter: createAskCursorAdapter({ model: "auto", timeout: CURSOR_TIMEOUT_MS }),
prompt: async (start: StartStep) =>
buildPlanPrompt({
threadId: start.meta.threadId,
nerveRoot,
}),
meta: zodMeta(planMetaSchema),
} satisfies RoleSpec<PlanMeta>;
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PlanMeta>> => { return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PlanMeta>> => {
const cwd = resolveRepoCwd(messages); const cwd = resolveRepoCwd(messages);
if (cwd === null) { if (cwd === null) {
@ -39,18 +31,43 @@ export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role<Plan
}; };
} }
const run = compileRoleSpec(innerSpec, { const innerSpec = {
provider, main: {
adapter: createCursorAdapter({
type: "cursor",
model: "auto",
timeout: CURSOR_TIMEOUT_MS,
mode: "ask",
}),
prompt: async (start: StartStep) =>
buildPlanPrompt({
threadId: start.meta.threadId,
nerveRoot,
}),
meta: zodMeta(planMetaSchema),
},
};
const compiled = compileWorkflowSpec(
{
name: "_plan-inner",
roles: innerSpec,
moderator: () => END,
},
{
extractFn: async <T>(raw: string, schema: Schema<T>, dryRun: boolean) =>
createLlmExtractFn<T>({ provider, dryRun })(raw, schema),
createContext: (s, m) => ({ createContext: (s, m) => ({
start: s, start: s,
messages: m, messages: m,
workdir: cwd, workdir: cwd,
signal: new AbortController().signal, signal: new AbortController().signal,
}), }),
}); },
);
try { try {
return await run(start, messages); return await compiled.roles.main(start, messages);
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e); const msg = e instanceof Error ? e.message : String(e);
return { return {

View File

@ -1,13 +1,13 @@
import { mkdirSync, writeFileSync } from "node:fs"; import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import type { Role, RoleResult, RoleSpec, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; import type { Role, RoleResult, Schema, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
import { END } from "@uncaged/nerve-core";
import { compileWorkflowSpec } from "@uncaged/nerve-daemon";
import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import { hermesAdapter } from "@uncaged/nerve-adapter-hermes";
import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { isDryRun } from "@uncaged/nerve-workflow-utils"; import { createLlmExtractFn, isDryRun, zodMeta } from "@uncaged/nerve-workflow-utils";
import { z } from "zod"; import { z } from "zod";
import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../../../_shared/rfc003-compile.js";
import { buildPublishPrompt } from "./prompt.js"; import { buildPublishPrompt } from "./prompt.js";
export const publishMetaSchema = z.object({ export const publishMetaSchema = z.object({
@ -20,21 +20,38 @@ export type BuildPublishDeps = {
nerveRoot: string; nerveRoot: string;
}; };
function defaultAgentCreateContext(nerveRoot: string) {
return (start: StartStep, messages: WorkflowMessage[]) => ({
start,
messages,
workdir: nerveRoot,
signal: new AbortController().signal,
});
}
function logPath(nerveRoot: string): string { function logPath(nerveRoot: string): string {
return join(nerveRoot, "logs", `solve-issue-publish-${Date.now()}.log`); return join(nerveRoot, "logs", `solve-issue-publish-${Date.now()}.log`);
} }
export function buildPublishRole({ provider, nerveRoot }: BuildPublishDeps): Role<PublishMeta> { export function buildPublishRole({ provider, nerveRoot }: BuildPublishDeps): Role<PublishMeta> {
const innerSpec = { const runHermes = compileWorkflowSpec(
{
name: "_publish-inner",
roles: {
main: {
adapter: hermesAdapter, adapter: hermesAdapter,
prompt: async (start: StartStep) => buildPublishPrompt({ threadId: start.meta.threadId, nerveRoot }), prompt: async (start: StartStep) => buildPublishPrompt({ threadId: start.meta.threadId, nerveRoot }),
meta: zodMeta(publishMetaSchema), meta: zodMeta(publishMetaSchema),
} satisfies RoleSpec<PublishMeta>; },
},
const runHermes = compileRoleSpec(innerSpec, { moderator: () => END,
provider, },
{
extractFn: async <T>(raw: string, schema: Schema<T>, dryRun: boolean) =>
createLlmExtractFn<T>({ provider, dryRun })(raw, schema),
createContext: defaultAgentCreateContext(nerveRoot), createContext: defaultAgentCreateContext(nerveRoot),
}); },
).roles.main;
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PublishMeta>> => { return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PublishMeta>> => {
const file = logPath(nerveRoot); const file = logPath(nerveRoot);