refactor(workflows): use createRole instead of compileWorkflowSpec

Switch build.ts and solve-issue inner roles to @uncaged/nerve-workflow-utils createRole with LlmExtractorConfig. Remove @uncaged/nerve-daemon from workspace dependencies; keep override for linking. Planner uses createCursorAdapter ask mode; dynamic cwd via start.meta.workdir.

Made-with: Cursor
This commit is contained in:
小橘 🍊(NEKO Team) 2026-04-29 10:01:02 +00:00 committed by 小橘
parent 70fd064bad
commit 95df8bc3c2
14 changed files with 187 additions and 361 deletions

View File

@ -10,7 +10,6 @@
"@uncaged/nerve-adapter-cursor": "link:../repos/nerve/packages/adapter-cursor", "@uncaged/nerve-adapter-cursor": "link:../repos/nerve/packages/adapter-cursor",
"@uncaged/nerve-adapter-hermes": "link:../repos/nerve/packages/adapter-hermes", "@uncaged/nerve-adapter-hermes": "link:../repos/nerve/packages/adapter-hermes",
"@uncaged/nerve-core": "latest", "@uncaged/nerve-core": "latest",
"@uncaged/nerve-daemon": "latest",
"@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils", "@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils",
"drizzle-orm": "latest", "drizzle-orm": "latest",
"zod": "^4.3.6" "zod": "^4.3.6"

12
pnpm-lock.yaml generated
View File

@ -24,9 +24,6 @@ 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
@ -112,9 +109,6 @@ 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
@ -143,9 +137,6 @@ 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
@ -174,9 +165,6 @@ 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,17 +1,8 @@
import type { import type { StartStep, WorkflowDefinition } from "@uncaged/nerve-core";
Schema, import { createCursorAdapter, cursorAdapter } from "@uncaged/nerve-adapter-cursor";
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 { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import { hermesAdapter } from "@uncaged/nerve-adapter-hermes";
import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils";
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";
@ -26,84 +17,54 @@ import { moderator } from "./moderator.js";
import type { SenseMeta } from "./moderator.js"; import type { SenseMeta } from "./moderator.js";
export type BuildSenseGeneratorDeps = { export type BuildSenseGeneratorDeps = {
provider: LlmProvider; extract: LlmExtractorConfig;
cwd: string; cwd: string;
}; };
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, extract,
cwd, cwd,
}: BuildSenseGeneratorDeps): WorkflowDefinition<SenseMeta> { }: BuildSenseGeneratorDeps): WorkflowDefinition<SenseMeta> {
const compileDeps: CompileWorkflowSpecDeps = { const roles = {
extractFn: async <T>(raw: string, schema: Schema<T>, dryRun: boolean) => planner: createRole(
createLlmExtractFn<T>({ provider, dryRun })(raw, schema), createCursorAdapter({
createContext: defaultAgentCreateContext(cwd), type: "cursor",
mode: "ask",
model: "auto",
timeout: CURSOR_TIMEOUT_MS,
}),
async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }),
plannerMetaSchema,
extract,
),
coder: createRole(
cursorAdapter,
async (start: StartStep) => coderPrompt({ threadId: start.meta.threadId }),
coderMetaSchema,
extract,
),
reviewer: createRole(
hermesAdapter,
async (start: StartStep) =>
reviewerPrompt({ threadId: start.meta.threadId, nerveRoot: cwd }),
reviewerMetaSchema,
extract,
),
tester: createRole(
hermesAdapter,
async (start: StartStep) =>
testerPrompt({ threadId: start.meta.threadId, nerveRoot: cwd }),
testerMetaSchema,
extract,
),
committer: buildCommitterRole({ nerveRoot: cwd }),
}; };
const agentSpec: WorkflowSpec<SenseAgentMeta> = {
name: "develop-sense",
roles: {
planner: {
adapter: createCursorAdapter({
type: "cursor",
model: "auto",
timeout: CURSOR_TIMEOUT_MS,
mode: "ask",
}),
prompt: async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }),
meta: zodMeta(plannerMetaSchema),
},
coder: {
adapter: createCursorAdapter({
type: "cursor",
model: "auto",
timeout: CURSOR_TIMEOUT_MS,
}),
prompt: async (start: StartStep) => coderPrompt({ threadId: start.meta.threadId }),
meta: zodMeta(coderMetaSchema),
},
reviewer: {
adapter: hermesAdapter,
prompt: async (start: StartStep) =>
reviewerPrompt({ threadId: start.meta.threadId, nerveRoot: cwd }),
meta: zodMeta(reviewerMetaSchema),
},
tester: {
adapter: hermesAdapter,
prompt: async (start: StartStep) =>
testerPrompt({ threadId: start.meta.threadId, nerveRoot: cwd }),
meta: zodMeta(testerMetaSchema),
},
},
moderator: () => END,
};
const compiled = compileWorkflowSpec(agentSpec, compileDeps);
return { return {
name: "develop-sense", name: "develop-sense",
roles: { roles,
planner: compiled.roles.planner,
coder: compiled.roles.coder,
reviewer: compiled.roles.reviewer,
tester: compiled.roles.tester,
committer: buildCommitterRole({ nerveRoot: cwd }),
},
moderator, moderator,
}; };
} }

View File

@ -12,7 +12,7 @@ if (!apiKey || !baseUrl) {
} }
const workflow = buildSenseGenerator({ const workflow = buildSenseGenerator({
provider: { apiKey, baseUrl, model }, extract: { provider: { apiKey, baseUrl, model } },
cwd: NERVE_ROOT, cwd: NERVE_ROOT,
}); });

View File

@ -10,7 +10,6 @@
"@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,17 +1,8 @@
import type { import type { StartStep, WorkflowDefinition } from "@uncaged/nerve-core";
Schema, import { createCursorAdapter, cursorAdapter } from "@uncaged/nerve-adapter-cursor";
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 { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import { hermesAdapter } from "@uncaged/nerve-adapter-hermes";
import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils";
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";
@ -26,84 +17,54 @@ import { moderator } from "./moderator.js";
import type { WorkflowMeta } from "./moderator.js"; import type { WorkflowMeta } from "./moderator.js";
export type BuildWorkflowGeneratorDeps = { export type BuildWorkflowGeneratorDeps = {
provider: LlmProvider; extract: LlmExtractorConfig;
nerveRoot: string; nerveRoot: string;
}; };
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, extract,
nerveRoot, nerveRoot,
}: BuildWorkflowGeneratorDeps): WorkflowDefinition<WorkflowMeta> { }: BuildWorkflowGeneratorDeps): WorkflowDefinition<WorkflowMeta> {
const compileDeps: CompileWorkflowSpecDeps = { const roles = {
extractFn: async <T>(raw: string, schema: Schema<T>, dryRun: boolean) => planner: createRole(
createLlmExtractFn<T>({ provider, dryRun })(raw, schema), createCursorAdapter({
createContext: defaultAgentCreateContext(nerveRoot), type: "cursor",
mode: "ask",
model: "auto",
timeout: CURSOR_TIMEOUT_MS,
}),
async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }),
plannerMetaSchema,
extract,
),
coder: createRole(
cursorAdapter,
async (start: StartStep) => coderPrompt({ threadId: start.meta.threadId }),
coderMetaSchema,
extract,
),
reviewer: createRole(
hermesAdapter,
async (start: StartStep) =>
reviewerPrompt({ threadId: start.meta.threadId, nerveRoot }),
reviewerMetaSchema,
extract,
),
tester: createRole(
hermesAdapter,
async (start: StartStep) =>
testerPrompt({ threadId: start.meta.threadId, nerveRoot }),
testerMetaSchema,
extract,
),
committer: buildCommitterRole({ nerveRoot }),
}; };
const agentSpec: WorkflowSpec<WorkflowAgentMeta> = {
name: "develop-workflow",
roles: {
planner: {
adapter: createCursorAdapter({
type: "cursor",
model: "auto",
timeout: CURSOR_TIMEOUT_MS,
mode: "ask",
}),
prompt: async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }),
meta: zodMeta(plannerMetaSchema),
},
coder: {
adapter: createCursorAdapter({
type: "cursor",
model: "auto",
timeout: CURSOR_TIMEOUT_MS,
}),
prompt: async (start: StartStep) => coderPrompt({ threadId: start.meta.threadId }),
meta: zodMeta(coderMetaSchema),
},
reviewer: {
adapter: hermesAdapter,
prompt: async (start: StartStep) =>
reviewerPrompt({ threadId: start.meta.threadId, nerveRoot }),
meta: zodMeta(reviewerMetaSchema),
},
tester: {
adapter: hermesAdapter,
prompt: async (start: StartStep) =>
testerPrompt({ threadId: start.meta.threadId, nerveRoot }),
meta: zodMeta(testerMetaSchema),
},
},
moderator: () => END,
};
const compiled = compileWorkflowSpec(agentSpec, compileDeps);
return { return {
name: "develop-workflow", name: "develop-workflow",
roles: { roles,
planner: compiled.roles.planner,
coder: compiled.roles.coder,
reviewer: compiled.roles.reviewer,
tester: compiled.roles.tester,
committer: buildCommitterRole({ nerveRoot }),
},
moderator, moderator,
}; };
} }

View File

@ -13,7 +13,7 @@ if (!apiKey || !baseUrl) {
} }
const workflow = buildWorkflowGenerator({ const workflow = buildWorkflowGenerator({
provider: { apiKey, baseUrl, model }, extract: { provider: { apiKey, baseUrl, model } },
nerveRoot: NERVE_ROOT, nerveRoot: NERVE_ROOT,
}); });

View File

@ -10,7 +10,6 @@
"@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,16 +1,7 @@
import type { import type { StartStep, WorkflowDefinition } from "@uncaged/nerve-core";
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 { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils";
import { moderator } from "./moderator.js"; import { moderator } from "./moderator.js";
import type { WorkflowMeta } from "./moderator.js"; import type { WorkflowMeta } from "./moderator.js";
@ -28,69 +19,44 @@ import { testPrompt } from "./roles/test/prompt.js";
export type BuildSolveIssueDeps = { export type BuildSolveIssueDeps = {
nerveRoot: string; nerveRoot: string;
provider: LlmProvider; extract: LlmExtractorConfig;
}; };
type SolveHermesAgentMeta = Pick<WorkflowMeta, "read-issue" | "prepare" | "review" | "test">; export function buildSolveIssue({
nerveRoot,
function defaultAgentCreateContext( extract,
workdir: string, }: BuildSolveIssueDeps): WorkflowDefinition<WorkflowMeta> {
): (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": {
adapter: hermesAdapter,
prompt: async (start: StartStep) => readIssuePrompt({ threadId: start.meta.threadId }),
meta: zodMeta(readIssueMetaSchema),
},
prepare: {
adapter: hermesAdapter,
prompt: async (start: StartStep) => preparePrompt({ threadId: start.meta.threadId }),
meta: zodMeta(prepareMetaSchema),
},
review: {
adapter: hermesAdapter,
prompt: async (start: StartStep) =>
reviewPrompt({ threadId: start.meta.threadId, nerveRoot }),
meta: zodMeta(reviewMetaSchema),
},
test: {
adapter: hermesAdapter,
prompt: async (start: StartStep) => testPrompt({ threadId: start.meta.threadId }),
meta: zodMeta(testMetaSchema),
},
},
moderator: () => END,
};
const compiled = compileWorkflowSpec(agentSpec, compileDeps);
return { return {
name: "solve-issue", name: "solve-issue",
roles: { roles: {
"read-issue": compiled.roles["read-issue"], "read-issue": createRole(
prepare: compiled.roles.prepare, hermesAdapter,
plan: buildPlanRole({ provider, nerveRoot }), async (start: StartStep) => readIssuePrompt({ threadId: start.meta.threadId }),
implement: buildImplementRole({ provider, nerveRoot }), readIssueMetaSchema,
review: compiled.roles.review, extract,
test: compiled.roles.test, ),
publish: buildPublishRole({ provider, nerveRoot }), prepare: createRole(
hermesAdapter,
async (start: StartStep) => preparePrompt({ threadId: start.meta.threadId }),
prepareMetaSchema,
extract,
),
plan: buildPlanRole({ extract, nerveRoot }),
implement: buildImplementRole({ extract, nerveRoot }),
review: createRole(
hermesAdapter,
async (start: StartStep) =>
reviewPrompt({ threadId: start.meta.threadId, nerveRoot }),
reviewMetaSchema,
extract,
),
test: createRole(
hermesAdapter,
async (start: StartStep) => testPrompt({ threadId: start.meta.threadId }),
testMetaSchema,
extract,
),
publish: buildPublishRole({ extract, nerveRoot }),
}, },
moderator, moderator,
}; };

View File

@ -11,6 +11,6 @@ if (provider === null) {
throw new Error("Set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL (or cfg get equivalents)"); throw new Error("Set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL (or cfg get equivalents)");
} }
const workflow = buildSolveIssue({ nerveRoot: NERVE_ROOT, provider }); const workflow = buildSolveIssue({ nerveRoot: NERVE_ROOT, extract: { provider } });
export default workflow; export default workflow;

View File

@ -10,7 +10,6 @@
"@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,9 +1,7 @@
import type { Role, RoleResult, Schema, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; import type { Role, RoleResult, 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 { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils"; import { createRole } 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";
@ -15,13 +13,16 @@ export const implementMetaSchema = z.object({
export type ImplementMeta = z.infer<typeof implementMetaSchema>; export type ImplementMeta = z.infer<typeof implementMetaSchema>;
export type BuildImplementDeps = { export type BuildImplementDeps = {
provider: LlmProvider; extract: LlmExtractorConfig;
nerveRoot: string; nerveRoot: string;
}; };
const CURSOR_TIMEOUT_MS = 300_000; const CURSOR_TIMEOUT_MS = 300_000;
export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps): Role<ImplementMeta> { export function buildImplementRole({
extract,
nerveRoot,
}: BuildImplementDeps): Role<ImplementMeta> {
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<ImplementMeta>> => { return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<ImplementMeta>> => {
const cwd = resolveRepoCwd(messages); const cwd = resolveRepoCwd(messages);
if (cwd === null) { if (cwd === null) {
@ -31,42 +32,28 @@ export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps):
}; };
} }
const innerSpec = { const innerRole = createRole(
main: { createCursorAdapter({
adapter: createCursorAdapter({ type: "cursor",
type: "cursor", model: "auto",
model: "auto", timeout: CURSOR_TIMEOUT_MS,
timeout: CURSOR_TIMEOUT_MS, }),
async (innerStart: StartStep) =>
buildImplementPrompt({
threadId: innerStart.meta.threadId,
nerveRoot,
}), }),
prompt: async (start: StartStep) => implementMetaSchema,
buildImplementPrompt({ extract,
threadId: start.meta.threadId,
nerveRoot,
}),
meta: zodMeta(implementMetaSchema),
},
};
const compiled = compileWorkflowSpec(
{
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) => ({
start: s,
messages: m,
workdir: cwd,
signal: new AbortController().signal,
}),
},
); );
const innerStart = {
...start,
meta: { ...start.meta, workdir: cwd },
} as StartStep;
try { try {
return await compiled.roles.main(start, messages); return await innerRole(innerStart, 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,9 +1,7 @@
import type { Role, RoleResult, Schema, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; import type { Role, RoleResult, 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 { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils"; import { createRole } 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";
@ -15,13 +13,13 @@ export const planMetaSchema = z.object({
export type PlanMeta = z.infer<typeof planMetaSchema>; export type PlanMeta = z.infer<typeof planMetaSchema>;
export type BuildPlanDeps = { export type BuildPlanDeps = {
provider: LlmProvider; extract: LlmExtractorConfig;
nerveRoot: string; nerveRoot: string;
}; };
const CURSOR_TIMEOUT_MS = 300_000; const CURSOR_TIMEOUT_MS = 300_000;
export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role<PlanMeta> { export function buildPlanRole({ extract, nerveRoot }: BuildPlanDeps): Role<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) {
@ -31,43 +29,29 @@ export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role<Plan
}; };
} }
const innerSpec = { const innerRole = createRole(
main: { createCursorAdapter({
adapter: createCursorAdapter({ type: "cursor",
type: "cursor", mode: "ask",
model: "auto", model: "auto",
timeout: CURSOR_TIMEOUT_MS, timeout: CURSOR_TIMEOUT_MS,
mode: "ask", }),
async (innerStart: StartStep) =>
buildPlanPrompt({
threadId: innerStart.meta.threadId,
nerveRoot,
}), }),
prompt: async (start: StartStep) => planMetaSchema,
buildPlanPrompt({ extract,
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) => ({
start: s,
messages: m,
workdir: cwd,
signal: new AbortController().signal,
}),
},
); );
const innerStart = {
...start,
meta: { ...start.meta, workdir: cwd },
} as StartStep;
try { try {
return await compiled.roles.main(start, messages); return await innerRole(innerStart, 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,11 +1,9 @@
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, Schema, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; import type { Role, RoleResult, 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 { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createLlmExtractFn, isDryRun, zodMeta } from "@uncaged/nerve-workflow-utils"; import { createRole, isDryRun } from "@uncaged/nerve-workflow-utils";
import { z } from "zod"; import { z } from "zod";
import { buildPublishPrompt } from "./prompt.js"; import { buildPublishPrompt } from "./prompt.js";
@ -16,42 +14,22 @@ export const publishMetaSchema = z.object({
export type PublishMeta = z.infer<typeof publishMetaSchema>; export type PublishMeta = z.infer<typeof publishMetaSchema>;
export type BuildPublishDeps = { export type BuildPublishDeps = {
provider: LlmProvider; extract: LlmExtractorConfig;
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({ extract, nerveRoot }: BuildPublishDeps): Role<PublishMeta> {
const runHermes = compileWorkflowSpec( const innerRole = createRole(
{ hermesAdapter,
name: "_publish-inner", async (start: StartStep) =>
roles: { buildPublishPrompt({ threadId: start.meta.threadId, nerveRoot }),
main: { publishMetaSchema,
adapter: hermesAdapter, extract,
prompt: async (start: StartStep) => buildPublishPrompt({ threadId: start.meta.threadId, nerveRoot }), );
meta: zodMeta(publishMetaSchema),
},
},
moderator: () => END,
},
{
extractFn: async <T>(raw: string, schema: Schema<T>, dryRun: boolean) =>
createLlmExtractFn<T>({ provider, dryRun })(raw, schema),
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);
@ -66,8 +44,13 @@ export function buildPublishRole({ provider, nerveRoot }: BuildPublishDeps): Rol
}; };
} }
const innerStart = {
...start,
meta: { ...start.meta, workdir: nerveRoot },
} as StartStep;
try { try {
return await runHermes(start, messages); return await innerRole(innerStart, messages);
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : String(e); const msg = e instanceof Error ? e.message : String(e);
const body = `publish failed: ${msg}\n`; const body = `publish failed: ${msg}\n`;