refactor: migrate all workflows to RFC-005 ThreadContext signatures
- Role: (start, messages) → (ctx: ThreadContext) - AgentFn prompt callbacks: (start) → (ctx) - ModeratorContext → ThreadContext - 13 files updated across knowledge-extraction and solve-issue workflows 小橘 <xiaoju@shazhou.work>
This commit is contained in:
parent
1c512435de
commit
e4fd5d6ba4
@ -20,7 +20,7 @@ const workflow = createKnowledgeExtractionWorkflow({
|
||||
adapters: {
|
||||
explorer: createCursorAdapter({
|
||||
type: "cursor",
|
||||
model: "auto",
|
||||
model: "claude-sonnet-4",
|
||||
timeout: CURSOR_TIMEOUT_MS,
|
||||
}),
|
||||
},
|
||||
|
||||
@ -2,7 +2,20 @@ import type { StartStep } from "@uncaged/nerve-core";
|
||||
|
||||
type StartMetaWithWorkdir = StartStep["meta"] & { workdir?: string | null };
|
||||
|
||||
/**
|
||||
* Resolve the target repo working directory.
|
||||
* Priority: start.meta.workdir → prompt second line (if absolute path) → cwd.
|
||||
*/
|
||||
export function resolveWorkdir(start: StartStep): string {
|
||||
const m = start.meta as StartMetaWithWorkdir;
|
||||
return m.workdir ?? process.cwd();
|
||||
if (m.workdir) return m.workdir;
|
||||
|
||||
// Allow prompt to carry workdir on the second line: "seed\n/abs/path"
|
||||
const lines = start.content.split(/\r?\n/);
|
||||
if (lines.length >= 2) {
|
||||
const candidate = lines[1]!.trim();
|
||||
if (candidate.startsWith("/")) return candidate;
|
||||
}
|
||||
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { END } from "@uncaged/nerve-core";
|
||||
import type { Moderator, ModeratorContext } from "@uncaged/nerve-core";
|
||||
import type { Moderator, ThreadContext } from "@uncaged/nerve-core";
|
||||
|
||||
import type { AnswererMeta } from "./roles/answerer.js";
|
||||
import type { ExplorerMeta } from "./roles/explorer.js";
|
||||
@ -11,7 +11,7 @@ export type WorkflowMeta = {
|
||||
explorer: ExplorerMeta;
|
||||
};
|
||||
|
||||
type Steps = ModeratorContext<WorkflowMeta>["steps"];
|
||||
type Steps = ThreadContext<WorkflowMeta>["steps"];
|
||||
|
||||
function lastQuestionerRemaining(steps: Steps): QuestionerMeta | undefined {
|
||||
for (let i = steps.length - 1; i >= 0; i--) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Role, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { Role, ThreadContext, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { llmExtract, nerveCommandEnv, spawnSafe } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
@ -37,8 +37,9 @@ function lastQuestionerMeta(messages: WorkflowMessage[]): QuestionerMeta | undef
|
||||
export function createAnswererRole(deps: CreateAnswererRoleDeps): Role<AnswererMeta> {
|
||||
const { extract } = deps;
|
||||
|
||||
return async (start: StartStep, messages: WorkflowMessage[]) => {
|
||||
const cwd = resolveWorkdir(start);
|
||||
return async (ctx: ThreadContext) => {
|
||||
const messages = ctx.steps as unknown as WorkflowMessage[];
|
||||
const cwd = resolveWorkdir(ctx.start);
|
||||
const qm = lastQuestionerMeta(messages);
|
||||
if (!qm || qm.questions.length === 0) {
|
||||
return {
|
||||
@ -49,7 +50,7 @@ export function createAnswererRole(deps: CreateAnswererRoleDeps): Role<AnswererM
|
||||
|
||||
const blocks: string[] = [];
|
||||
for (const q of qm.questions) {
|
||||
if (start.meta.dryRun) {
|
||||
if ((ctx.start.meta as Record<string, unknown>).dryRun) {
|
||||
blocks.push(`### ${q.id}\n[dryRun] skipped nerve knowledge query\n`);
|
||||
continue;
|
||||
}
|
||||
@ -93,7 +94,7 @@ export function createAnswererRole(deps: CreateAnswererRoleDeps): Role<AnswererM
|
||||
text: bundle,
|
||||
schema: answererMetaSchema,
|
||||
provider: extract.provider,
|
||||
dryRun: start.meta.dryRun,
|
||||
dryRun: false,
|
||||
});
|
||||
if (!metaR.ok) {
|
||||
throw new Error(`answerer llmExtract: ${JSON.stringify(metaR.error)}`);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AgentFn, Role, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { AgentFn, Role, ThreadContext, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
@ -33,11 +33,12 @@ function lastMeta<M>(messages: WorkflowMessage[], role: string): M | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function explorerPrompt(start: StartStep, messages: WorkflowMessage[]): string {
|
||||
const threadId = start.meta.threadId;
|
||||
export function explorerPrompt(ctx: ThreadContext): string {
|
||||
const messages = ctx.steps as unknown as WorkflowMessage[];
|
||||
const threadId = ctx.start.meta.threadId;
|
||||
const qm = lastMeta<QuestionerMeta>(messages, "questioner");
|
||||
const am = lastMeta<AnswererMeta>(messages, "answerer");
|
||||
const cwd = resolveWorkdir(start);
|
||||
const cwd = resolveWorkdir(ctx.start);
|
||||
|
||||
const unanswered =
|
||||
am?.results.filter((r) => !r.found).map((r) => r.id) ?? [];
|
||||
@ -85,7 +86,7 @@ export function createExplorerRole(
|
||||
): Role<ExplorerMeta> {
|
||||
return createRole(
|
||||
adapter,
|
||||
async (innerStart: StartStep, msgs: WorkflowMessage[]) => explorerPrompt(innerStart, msgs),
|
||||
async (ctx: ThreadContext) => explorerPrompt(ctx),
|
||||
explorerMetaSchema,
|
||||
extract,
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { Role, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { Role, ThreadContext, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createLlmRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
@ -18,7 +18,7 @@ const questionerExtractSchema = z.object({
|
||||
domain: z.string(),
|
||||
}),
|
||||
)
|
||||
.length(3),
|
||||
.length(5),
|
||||
});
|
||||
|
||||
export type QuestionerMeta = {
|
||||
@ -35,11 +35,11 @@ export type CreateQuestionerRoleDeps = {
|
||||
function questionerSystem(): string {
|
||||
return `You are the **questioner** in a knowledge-extraction workflow.
|
||||
|
||||
Read the given markdown knowledge card. Propose exactly **three** technical questions that are **not** already answered or covered by that card.
|
||||
Read the given markdown knowledge card. Propose exactly **five** technical questions that are **not** already answered or covered by that card.
|
||||
|
||||
Rules:
|
||||
- Questions must be concrete and technical.
|
||||
- Each question needs a stable string id (e.g. q1, q2, q3), a short domain label (e.g. routing, storage), and the question text.
|
||||
- Each question needs a stable string id (e.g. q1, q2, q3, q4, q5), a short domain label (e.g. routing, storage), and the question text.
|
||||
- Do not assume access to other files or tools — reason only from the card content shown.`;
|
||||
}
|
||||
|
||||
@ -56,9 +56,10 @@ ${cardBody}`;
|
||||
export function createQuestionerRole(adapterExtract: CreateQuestionerRoleDeps): Role<QuestionerMeta> {
|
||||
const { extract } = adapterExtract;
|
||||
|
||||
return async (start: StartStep, messages: WorkflowMessage[]) => {
|
||||
const cwd = resolveWorkdir(start);
|
||||
const queue = await resolveQueueForQuestioner(start, messages, cwd);
|
||||
return async (ctx: ThreadContext) => {
|
||||
const messages = ctx.steps as unknown as WorkflowMessage[];
|
||||
const cwd = resolveWorkdir(ctx.start);
|
||||
const queue = await resolveQueueForQuestioner(ctx.start, messages, cwd);
|
||||
if (queue.length === 0) {
|
||||
return {
|
||||
content:
|
||||
@ -93,7 +94,7 @@ export function createQuestionerRole(adapterExtract: CreateQuestionerRoleDeps):
|
||||
},
|
||||
});
|
||||
|
||||
const r = await inner(start, messages);
|
||||
const r = await inner(ctx);
|
||||
return {
|
||||
content: r.content,
|
||||
meta: {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { join } from "node:path";
|
||||
import type { WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { RoleStep, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
|
||||
type SolveIssueParse = {
|
||||
host: string;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole, decorateRole, withDryRun, onFail } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
@ -18,7 +18,7 @@ export function createCommitterRole(
|
||||
): Role<CommitterMeta> {
|
||||
const inner = createRole(
|
||||
adapter,
|
||||
async (start: StartStep) => committerPrompt({ threadId: start.meta.threadId }),
|
||||
async (ctx: ThreadContext) => committerPrompt({ threadId: ctx.start.meta.threadId }),
|
||||
committerMetaSchema,
|
||||
extract,
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AgentFn, Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { AgentFn, Role, RoleResult, ThreadContext, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
@ -20,7 +20,8 @@ export function createImplementRole(
|
||||
adapter: AgentFn,
|
||||
{ extract, nerveRoot }: CreateImplementRoleDeps,
|
||||
): Role<ImplementMeta> {
|
||||
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<ImplementMeta>> => {
|
||||
return async (ctx: ThreadContext): Promise<RoleResult<ImplementMeta>> => {
|
||||
const messages = ctx.steps as unknown as WorkflowMessage[];
|
||||
const cwd = resolveRepoCwd(messages);
|
||||
if (cwd === null) {
|
||||
return {
|
||||
@ -31,22 +32,24 @@ export function createImplementRole(
|
||||
|
||||
const innerRole = createRole(
|
||||
adapter,
|
||||
async (innerStart: StartStep) =>
|
||||
async (innerCtx: ThreadContext) =>
|
||||
buildImplementPrompt({
|
||||
threadId: innerStart.meta.threadId,
|
||||
threadId: innerCtx.start.meta.threadId,
|
||||
nerveRoot,
|
||||
}),
|
||||
implementMetaSchema,
|
||||
extract,
|
||||
);
|
||||
|
||||
const innerStart = {
|
||||
...start,
|
||||
meta: { ...start.meta, workdir: cwd },
|
||||
} as StartStep;
|
||||
|
||||
const innerCtx: ThreadContext = {
|
||||
...ctx,
|
||||
start: {
|
||||
...ctx.start,
|
||||
meta: { ...ctx.start.meta, workdir: cwd },
|
||||
},
|
||||
};
|
||||
try {
|
||||
return await innerRole(innerStart, messages);
|
||||
return await innerRole(innerCtx);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AgentFn, Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { AgentFn, Role, RoleResult, ThreadContext, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
@ -20,7 +20,8 @@ export function createPlanRole(
|
||||
adapter: AgentFn,
|
||||
{ extract, nerveRoot }: CreatePlanRoleDeps,
|
||||
): Role<PlanMeta> {
|
||||
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PlanMeta>> => {
|
||||
return async (ctx: ThreadContext): Promise<RoleResult<PlanMeta>> => {
|
||||
const messages = ctx.steps as unknown as WorkflowMessage[];
|
||||
const cwd = resolveRepoCwd(messages);
|
||||
if (cwd === null) {
|
||||
return {
|
||||
@ -31,22 +32,24 @@ export function createPlanRole(
|
||||
|
||||
const innerRole = createRole(
|
||||
adapter,
|
||||
async (innerStart: StartStep) =>
|
||||
async (innerCtx: ThreadContext) =>
|
||||
buildPlanPrompt({
|
||||
threadId: innerStart.meta.threadId,
|
||||
threadId: innerCtx.start.meta.threadId,
|
||||
nerveRoot,
|
||||
}),
|
||||
planMetaSchema,
|
||||
extract,
|
||||
);
|
||||
|
||||
const innerStart = {
|
||||
...start,
|
||||
meta: { ...start.meta, workdir: cwd },
|
||||
} as StartStep;
|
||||
|
||||
const innerCtx: ThreadContext = {
|
||||
...ctx,
|
||||
start: {
|
||||
...ctx.start,
|
||||
meta: { ...ctx.start.meta, workdir: cwd },
|
||||
},
|
||||
};
|
||||
try {
|
||||
return await innerRole(innerStart, messages);
|
||||
return await innerRole(innerCtx);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
@ -13,7 +13,7 @@ export type PrepareMeta = z.infer<typeof prepareMetaSchema>;
|
||||
export function createPrepareRole(adapter: AgentFn, extract: LlmExtractorConfig): Role<PrepareMeta> {
|
||||
return createRole(
|
||||
adapter,
|
||||
async (start: StartStep) => preparePrompt({ threadId: start.meta.threadId }),
|
||||
async (ctx: ThreadContext) => preparePrompt({ threadId: ctx.start.meta.threadId }),
|
||||
prepareMetaSchema,
|
||||
extract,
|
||||
);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { AgentFn, Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { AgentFn, Role, RoleResult, ThreadContext, WorkflowMessage } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole, isDryRun } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
@ -27,17 +27,17 @@ export function createPublishRole(
|
||||
): Role<PublishMeta> {
|
||||
const innerRole = createRole(
|
||||
adapter,
|
||||
async (start: StartStep) =>
|
||||
buildPublishPrompt({ threadId: start.meta.threadId, nerveRoot }),
|
||||
async (ctx: ThreadContext) =>
|
||||
buildPublishPrompt({ threadId: ctx.start.meta.threadId, nerveRoot }),
|
||||
publishMetaSchema,
|
||||
extract,
|
||||
);
|
||||
|
||||
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PublishMeta>> => {
|
||||
return async (ctx: ThreadContext): Promise<RoleResult<PublishMeta>> => {
|
||||
const file = logPath(nerveRoot);
|
||||
mkdirSync(join(file, ".."), { recursive: true });
|
||||
|
||||
if (isDryRun(start)) {
|
||||
if (isDryRun(ctx.start)) {
|
||||
const msg = "[dry-run] publish skipped (no git push / PR)";
|
||||
writeFileSync(file, `${msg}\n`, "utf-8");
|
||||
return {
|
||||
@ -46,13 +46,16 @@ export function createPublishRole(
|
||||
};
|
||||
}
|
||||
|
||||
const innerStart = {
|
||||
...start,
|
||||
meta: { ...start.meta, workdir: nerveRoot },
|
||||
} as StartStep;
|
||||
const innerCtx: ThreadContext = {
|
||||
...ctx,
|
||||
start: {
|
||||
...ctx.start,
|
||||
meta: { ...ctx.start.meta, workdir: nerveRoot },
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
return await innerRole(innerStart, messages);
|
||||
return await innerRole(innerCtx);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
const body = `publish failed: ${msg}\n`;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
@ -13,7 +13,7 @@ export type ReadIssueMeta = z.infer<typeof readIssueMetaSchema>;
|
||||
export function createReadIssueRole(adapter: AgentFn, extract: LlmExtractorConfig): Role<ReadIssueMeta> {
|
||||
return createRole(
|
||||
adapter,
|
||||
async (start: StartStep) => readIssuePrompt({ threadId: start.meta.threadId }),
|
||||
async (ctx: ThreadContext) => readIssuePrompt({ threadId: ctx.start.meta.threadId }),
|
||||
readIssueMetaSchema,
|
||||
extract,
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
@ -17,8 +17,8 @@ export function createReviewRole(
|
||||
): Role<ReviewMeta> {
|
||||
return createRole(
|
||||
adapter,
|
||||
async (start: StartStep) =>
|
||||
reviewPrompt({ threadId: start.meta.threadId, nerveRoot }),
|
||||
async (ctx: ThreadContext) =>
|
||||
reviewPrompt({ threadId: ctx.start.meta.threadId, nerveRoot }),
|
||||
reviewMetaSchema,
|
||||
extract,
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||
import type { AgentFn, Role, ThreadContext } from "@uncaged/nerve-core";
|
||||
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||
import { z } from "zod";
|
||||
@ -13,7 +13,7 @@ export type TestMeta = z.infer<typeof testMetaSchema>;
|
||||
export function createTestRole(adapter: AgentFn, extract: LlmExtractorConfig): Role<TestMeta> {
|
||||
return createRole(
|
||||
adapter,
|
||||
async (start: StartStep) => testPrompt({ threadId: start.meta.threadId }),
|
||||
async (ctx: ThreadContext) => testPrompt({ threadId: ctx.start.meta.threadId }),
|
||||
testMetaSchema,
|
||||
extract,
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user