124 lines
3.6 KiB
TypeScript
124 lines
3.6 KiB
TypeScript
import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
|
|
import { cursorAgent, llmExtract } from "@uncaged/nerve-workflow-utils";
|
|
import { z } from "zod";
|
|
import { readNerveConfigText } from "../../lib/nerve-read.js";
|
|
import { resolveDashScopeProvider } from "../../lib/provider.js";
|
|
import { lastMetaForRole } from "../../lib/meta-helpers.js";
|
|
import { formatSpawnFailure } from "../../lib/spawn-utils.js";
|
|
import type { IssueReaderMeta } from "../issue-reader/index.js";
|
|
import { buildPlannerPrompt } from "./prompt.js";
|
|
|
|
export type PlannerMeta = {
|
|
plan: string | null;
|
|
targetFiles: string[] | null;
|
|
testCommands: string[] | null;
|
|
riskNotes: string[] | null;
|
|
planningOk: boolean;
|
|
failureKind: "none" | "planning_failed";
|
|
failureReason: string | null;
|
|
};
|
|
|
|
const planSchema = z.object({
|
|
targetFiles: z.array(z.string().default("")).default([]),
|
|
testCommands: z.array(z.string().default("")).default([]),
|
|
riskNotes: z.array(z.string().default("")).default([]),
|
|
});
|
|
|
|
export type BuildPlannerDeps = {
|
|
nerveRoot: string;
|
|
};
|
|
|
|
export function buildPlannerRole({ nerveRoot }: BuildPlannerDeps): Role<PlannerMeta> {
|
|
return async (_start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PlannerMeta>> => {
|
|
const issueMeta = lastMetaForRole<IssueReaderMeta>(messages, "issue-reader");
|
|
if (issueMeta === null || !issueMeta.fetchOk || issueMeta.issue === null) {
|
|
return {
|
|
content: "planner cannot continue: issue-reader has no issue data",
|
|
meta: {
|
|
plan: null,
|
|
targetFiles: null,
|
|
testCommands: null,
|
|
riskNotes: null,
|
|
planningOk: false,
|
|
failureKind: "planning_failed",
|
|
failureReason: "missing issue data",
|
|
},
|
|
};
|
|
}
|
|
|
|
const prompt = buildPlannerPrompt({
|
|
issue: issueMeta.issue,
|
|
nerveYamlText: readNerveConfigText(nerveRoot),
|
|
});
|
|
|
|
const result = await cursorAgent({
|
|
prompt,
|
|
mode: "ask",
|
|
model: "auto",
|
|
cwd: nerveRoot,
|
|
env: null,
|
|
timeoutMs: null,
|
|
});
|
|
if (!result.ok) {
|
|
return {
|
|
content: `planner failed: ${formatSpawnFailure(result.error)}`,
|
|
meta: {
|
|
plan: null,
|
|
targetFiles: null,
|
|
testCommands: null,
|
|
riskNotes: null,
|
|
planningOk: false,
|
|
failureKind: "planning_failed",
|
|
failureReason: formatSpawnFailure(result.error),
|
|
},
|
|
};
|
|
}
|
|
|
|
const plan = result.value;
|
|
const provider = await resolveDashScopeProvider(nerveRoot);
|
|
if (provider === null) {
|
|
return {
|
|
content: plan,
|
|
meta: {
|
|
plan,
|
|
targetFiles: null,
|
|
testCommands: null,
|
|
riskNotes: null,
|
|
planningOk: true,
|
|
failureKind: "none",
|
|
failureReason: null,
|
|
},
|
|
};
|
|
}
|
|
|
|
const structured = await llmExtract({ text: plan, schema: planSchema, provider });
|
|
if (!structured.ok) {
|
|
return {
|
|
content: `${plan}\n\n[llmExtract error] ${JSON.stringify(structured.error)}`,
|
|
meta: {
|
|
plan,
|
|
targetFiles: null,
|
|
testCommands: null,
|
|
riskNotes: null,
|
|
planningOk: true,
|
|
failureKind: "none",
|
|
failureReason: null,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
content: plan,
|
|
meta: {
|
|
plan,
|
|
targetFiles: structured.value.targetFiles,
|
|
testCommands: structured.value.testCommands,
|
|
riskNotes: structured.value.riskNotes,
|
|
planningOk: true,
|
|
failureKind: "none",
|
|
failureReason: null,
|
|
},
|
|
};
|
|
};
|
|
}
|