diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62f66df..08cb6a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,6 +112,9 @@ importers: '@uncaged/nerve-core': specifier: 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': specifier: link:../../../repos/nerve/packages/workflow-utils version: link:../../../repos/nerve/packages/workflow-utils @@ -140,6 +143,9 @@ importers: '@uncaged/nerve-core': specifier: 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': specifier: link:../../../repos/nerve/packages/workflow-utils version: link:../../../repos/nerve/packages/workflow-utils @@ -168,6 +174,9 @@ importers: '@uncaged/nerve-core': specifier: 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': specifier: link:../../../repos/nerve/packages/workflow-utils version: link:../../../repos/nerve/packages/workflow-utils diff --git a/workflows/_shared/cursor-ask-adapter.ts b/workflows/_shared/cursor-ask-adapter.ts deleted file mode 100644 index 88cf3fb..0000000 --- a/workflows/_shared/cursor-ask-adapter.ts +++ /dev/null @@ -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 => { - 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; - }; -} diff --git a/workflows/_shared/rfc003-compile.ts b/workflows/_shared/rfc003-compile.ts deleted file mode 100644 index c6962b8..0000000 --- a/workflows/_shared/rfc003-compile.ts +++ /dev/null @@ -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(zod: z.ZodType): ZodMetaSchema { - 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>( - spec: RoleSpec, - deps: CompileRoleSpecDeps, -): Role { - 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).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, - }); -} diff --git a/workflows/develop-sense/build.ts b/workflows/develop-sense/build.ts index d8c26cf..5649151 100644 --- a/workflows/develop-sense/build.ts +++ b/workflows/develop-sense/build.ts @@ -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 { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; - -import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../_shared/rfc003-compile.js"; -import { createAskCursorAdapter } from "../_shared/cursor-ask-adapter.js"; +import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils"; import { coderPrompt } from "./roles/coder/prompt.js"; import { coderMetaSchema } from "./roles/coder/index.js"; @@ -25,49 +32,76 @@ export type BuildSenseGeneratorDeps = { const CURSOR_TIMEOUT_MS = 300_000; +type SenseAgentMeta = Pick; + +function defaultAgentCreateContext( + workdir: string, +): (start: StartStep, messages: WorkflowMessage[]) => WorkflowContext { + return (start, messages) => ({ + start, + messages, + workdir, + signal: new AbortController().signal, + }); +} + export function buildSenseGenerator({ provider, cwd, }: BuildSenseGeneratorDeps): WorkflowDefinition { - const createContext = defaultAgentCreateContext(cwd); - const deps = { provider, createContext }; - - const agentRoles = { - planner: { - adapter: createAskCursorAdapter({ model: "auto", timeout: CURSOR_TIMEOUT_MS }), - 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), - }, + const compileDeps: CompileWorkflowSpecDeps = { + extractFn: async (raw: string, schema: Schema, dryRun: boolean) => + createLlmExtractFn({ provider, dryRun })(raw, schema), + createContext: defaultAgentCreateContext(cwd), }; + const agentSpec: WorkflowSpec = { + 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 { name: "develop-sense", roles: { - planner: compileRoleSpec(agentRoles.planner, deps), - coder: compileRoleSpec(agentRoles.coder, deps), - reviewer: compileRoleSpec(agentRoles.reviewer, deps), - tester: compileRoleSpec(agentRoles.tester, deps), + planner: compiled.roles.planner, + coder: compiled.roles.coder, + reviewer: compiled.roles.reviewer, + tester: compiled.roles.tester, committer: buildCommitterRole({ nerveRoot: cwd }), }, moderator, diff --git a/workflows/develop-sense/package.json b/workflows/develop-sense/package.json index dff8549..43e2f26 100644 --- a/workflows/develop-sense/package.json +++ b/workflows/develop-sense/package.json @@ -10,6 +10,7 @@ "@uncaged/nerve-adapter-cursor": "latest", "@uncaged/nerve-adapter-hermes": "latest", "@uncaged/nerve-core": "latest", + "@uncaged/nerve-daemon": "latest", "@uncaged/nerve-workflow-utils": "latest", "zod": "^4.3.6" }, diff --git a/workflows/develop-workflow/build.ts b/workflows/develop-workflow/build.ts index 4716460..bb31777 100644 --- a/workflows/develop-workflow/build.ts +++ b/workflows/develop-workflow/build.ts @@ -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 { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; - -import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../_shared/rfc003-compile.js"; -import { createAskCursorAdapter } from "../_shared/cursor-ask-adapter.js"; +import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils"; import { coderPrompt } from "./roles/coder/prompt.js"; import { coderMetaSchema } from "./roles/coder/index.js"; @@ -25,49 +32,76 @@ export type BuildWorkflowGeneratorDeps = { const CURSOR_TIMEOUT_MS = 300_000; +type WorkflowAgentMeta = Pick; + +function defaultAgentCreateContext( + workdir: string, +): (start: StartStep, messages: WorkflowMessage[]) => WorkflowContext { + return (start, messages) => ({ + start, + messages, + workdir, + signal: new AbortController().signal, + }); +} + export function buildWorkflowGenerator({ provider, nerveRoot, }: BuildWorkflowGeneratorDeps): WorkflowDefinition { - const createContext = defaultAgentCreateContext(nerveRoot); - const deps = { provider, createContext }; - - const agentRoles = { - planner: { - adapter: createAskCursorAdapter({ model: "auto", timeout: CURSOR_TIMEOUT_MS }), - 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), - }, + const compileDeps: CompileWorkflowSpecDeps = { + extractFn: async (raw: string, schema: Schema, dryRun: boolean) => + createLlmExtractFn({ provider, dryRun })(raw, schema), + createContext: defaultAgentCreateContext(nerveRoot), }; + const agentSpec: WorkflowSpec = { + 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 { name: "develop-workflow", roles: { - planner: compileRoleSpec(agentRoles.planner, deps), - coder: compileRoleSpec(agentRoles.coder, deps), - reviewer: compileRoleSpec(agentRoles.reviewer, deps), - tester: compileRoleSpec(agentRoles.tester, deps), + planner: compiled.roles.planner, + coder: compiled.roles.coder, + reviewer: compiled.roles.reviewer, + tester: compiled.roles.tester, committer: buildCommitterRole({ nerveRoot }), }, moderator, diff --git a/workflows/develop-workflow/package.json b/workflows/develop-workflow/package.json index 336bcf0..895d8fa 100644 --- a/workflows/develop-workflow/package.json +++ b/workflows/develop-workflow/package.json @@ -10,6 +10,7 @@ "@uncaged/nerve-adapter-cursor": "latest", "@uncaged/nerve-adapter-hermes": "latest", "@uncaged/nerve-core": "latest", + "@uncaged/nerve-daemon": "latest", "@uncaged/nerve-workflow-utils": "latest", "zod": "^4.3.6" }, diff --git a/workflows/solve-issue/build.ts b/workflows/solve-issue/build.ts index cda118d..fdac54d 100644 --- a/workflows/solve-issue/build.ts +++ b/workflows/solve-issue/build.ts @@ -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 type { LlmProvider } from "@uncaged/nerve-workflow-utils"; - -import { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../_shared/rfc003-compile.js"; +import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils"; import { moderator } from "./moderator.js"; import type { WorkflowMeta } from "./moderator.js"; @@ -23,43 +31,65 @@ export type BuildSolveIssueDeps = { provider: LlmProvider; }; -export function buildSolveIssue({ nerveRoot, provider }: BuildSolveIssueDeps): WorkflowDefinition { - const createContext = defaultAgentCreateContext(nerveRoot); - const deps = { provider, createContext }; +type SolveHermesAgentMeta = Pick; - const agentRoles = { - "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), - }, +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 { + const compileDeps: CompileWorkflowSpecDeps = { + extractFn: async (raw: string, schema: Schema, dryRun: boolean) => + createLlmExtractFn({ provider, dryRun })(raw, schema), + createContext: defaultAgentCreateContext(nerveRoot), }; + const agentSpec: WorkflowSpec = { + 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 { name: "solve-issue", roles: { - "read-issue": compileRoleSpec(agentRoles["read-issue"], deps), - prepare: compileRoleSpec(agentRoles.prepare, deps), + "read-issue": compiled.roles["read-issue"], + prepare: compiled.roles.prepare, plan: buildPlanRole({ provider, nerveRoot }), implement: buildImplementRole({ provider, nerveRoot }), - review: compileRoleSpec(agentRoles.review, deps), - test: compileRoleSpec(agentRoles.test, deps), + review: compiled.roles.review, + test: compiled.roles.test, publish: buildPublishRole({ provider, nerveRoot }), }, moderator, diff --git a/workflows/solve-issue/package.json b/workflows/solve-issue/package.json index ce6e1eb..a05fb08 100644 --- a/workflows/solve-issue/package.json +++ b/workflows/solve-issue/package.json @@ -10,6 +10,7 @@ "@uncaged/nerve-adapter-cursor": "latest", "@uncaged/nerve-adapter-hermes": "latest", "@uncaged/nerve-core": "latest", + "@uncaged/nerve-daemon": "latest", "@uncaged/nerve-workflow-utils": "latest", "zod": "^4.3.6" }, diff --git a/workflows/solve-issue/roles/implement/index.ts b/workflows/solve-issue/roles/implement/index.ts index 51c29be..b739e42 100644 --- a/workflows/solve-issue/roles/implement/index.ts +++ b/workflows/solve-issue/roles/implement/index.ts @@ -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 type { LlmProvider } from "@uncaged/nerve-workflow-utils"; +import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; -import { compileRoleSpec, zodMeta } from "../../../_shared/rfc003-compile.js"; - import { resolveRepoCwd } from "../../lib/repo-context.js"; import { buildImplementPrompt } from "./prompt.js"; @@ -21,20 +22,6 @@ export type BuildImplementDeps = { const CURSOR_TIMEOUT_MS = 300_000; export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps): Role { - const innerSpec = { - adapter: createCursorAdapter({ - type: "cursor", - model: "auto", - timeout: CURSOR_TIMEOUT_MS, - }), - prompt: async (start: StartStep) => - buildImplementPrompt({ - threadId: start.meta.threadId, - nerveRoot, - }), - meta: zodMeta(implementMetaSchema), - } satisfies RoleSpec; - return async (start: StartStep, messages: WorkflowMessage[]): Promise> => { const cwd = resolveRepoCwd(messages); if (cwd === null) { @@ -44,18 +31,42 @@ export function buildImplementRole({ provider, nerveRoot }: BuildImplementDeps): }; } - const run = compileRoleSpec(innerSpec, { - provider, - createContext: (s, m) => ({ - start: s, - messages: m, - workdir: cwd, - signal: new AbortController().signal, - }), - }); + const innerSpec = { + main: { + adapter: createCursorAdapter({ + type: "cursor", + model: "auto", + timeout: CURSOR_TIMEOUT_MS, + }), + prompt: async (start: StartStep) => + buildImplementPrompt({ + threadId: start.meta.threadId, + nerveRoot, + }), + meta: zodMeta(implementMetaSchema), + }, + }; + + const compiled = compileWorkflowSpec( + { + name: "_implement-inner", + roles: innerSpec, + moderator: () => END, + }, + { + extractFn: async (raw: string, schema: Schema, dryRun: boolean) => + createLlmExtractFn({ provider, dryRun })(raw, schema), + createContext: (s, m) => ({ + start: s, + messages: m, + workdir: cwd, + signal: new AbortController().signal, + }), + }, + ); try { - return await run(start, messages); + return await compiled.roles.main(start, messages); } catch (e) { const msg = e instanceof Error ? e.message : String(e); return { diff --git a/workflows/solve-issue/roles/plan/index.ts b/workflows/solve-issue/roles/plan/index.ts index 827dbb6..192ce88 100644 --- a/workflows/solve-issue/roles/plan/index.ts +++ b/workflows/solve-issue/roles/plan/index.ts @@ -1,7 +1,9 @@ -import type { Role, RoleResult, RoleSpec, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; -import { createAskCursorAdapter } from "../../../_shared/cursor-ask-adapter.js"; -import { compileRoleSpec, zodMeta } from "../../../_shared/rfc003-compile.js"; +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 type { LlmProvider } from "@uncaged/nerve-workflow-utils"; +import { createLlmExtractFn, zodMeta } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; import { resolveRepoCwd } from "../../lib/repo-context.js"; @@ -20,16 +22,6 @@ export type BuildPlanDeps = { const CURSOR_TIMEOUT_MS = 300_000; export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role { - 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; - return async (start: StartStep, messages: WorkflowMessage[]): Promise> => { const cwd = resolveRepoCwd(messages); if (cwd === null) { @@ -39,18 +31,43 @@ export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role ({ - start: s, - messages: m, - workdir: cwd, - signal: new AbortController().signal, - }), - }); + const innerSpec = { + 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 (raw: string, schema: Schema, dryRun: boolean) => + createLlmExtractFn({ provider, dryRun })(raw, schema), + createContext: (s, m) => ({ + start: s, + messages: m, + workdir: cwd, + signal: new AbortController().signal, + }), + }, + ); try { - return await run(start, messages); + return await compiled.roles.main(start, messages); } catch (e) { const msg = e instanceof Error ? e.message : String(e); return { diff --git a/workflows/solve-issue/roles/publish/index.ts b/workflows/solve-issue/roles/publish/index.ts index 762b167..d42ad18 100644 --- a/workflows/solve-issue/roles/publish/index.ts +++ b/workflows/solve-issue/roles/publish/index.ts @@ -1,13 +1,13 @@ import { mkdirSync, writeFileSync } from "node:fs"; 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 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 { compileRoleSpec, defaultAgentCreateContext, zodMeta } from "../../../_shared/rfc003-compile.js"; - import { buildPublishPrompt } from "./prompt.js"; export const publishMetaSchema = z.object({ @@ -20,21 +20,38 @@ export type BuildPublishDeps = { nerveRoot: string; }; +function defaultAgentCreateContext(nerveRoot: string) { + return (start: StartStep, messages: WorkflowMessage[]) => ({ + start, + messages, + workdir: nerveRoot, + signal: new AbortController().signal, + }); +} + function logPath(nerveRoot: string): string { return join(nerveRoot, "logs", `solve-issue-publish-${Date.now()}.log`); } export function buildPublishRole({ provider, nerveRoot }: BuildPublishDeps): Role { - const innerSpec = { - adapter: hermesAdapter, - prompt: async (start: StartStep) => buildPublishPrompt({ threadId: start.meta.threadId, nerveRoot }), - meta: zodMeta(publishMetaSchema), - } satisfies RoleSpec; - - const runHermes = compileRoleSpec(innerSpec, { - provider, - createContext: defaultAgentCreateContext(nerveRoot), - }); + const runHermes = compileWorkflowSpec( + { + name: "_publish-inner", + roles: { + main: { + adapter: hermesAdapter, + prompt: async (start: StartStep) => buildPublishPrompt({ threadId: start.meta.threadId, nerveRoot }), + meta: zodMeta(publishMetaSchema), + }, + }, + moderator: () => END, + }, + { + extractFn: async (raw: string, schema: Schema, dryRun: boolean) => + createLlmExtractFn({ provider, dryRun })(raw, schema), + createContext: defaultAgentCreateContext(nerveRoot), + }, + ).roles.main; return async (start: StartStep, messages: WorkflowMessage[]): Promise> => { const file = logPath(nerveRoot);