From e4fd5d6ba487cd151b1274596252d489f13a4437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Thu, 30 Apr 2026 08:39:52 +0000 Subject: [PATCH] refactor: migrate all workflows to RFC-005 ThreadContext signatures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Role: (start, messages) → (ctx: ThreadContext) - AgentFn prompt callbacks: (start) → (ctx) - ModeratorContext → ThreadContext - 13 files updated across knowledge-extraction and solve-issue workflows 小橘 --- workflows/knowledge-extraction/index.ts | 2 +- workflows/knowledge-extraction/lib/workdir.ts | 15 +++++++++++- workflows/knowledge-extraction/moderator.ts | 4 ++-- .../knowledge-extraction/roles/answerer.ts | 11 +++++---- .../knowledge-extraction/roles/explorer.ts | 11 +++++---- .../knowledge-extraction/roles/questioner.ts | 17 +++++++------- workflows/solve-issue/lib/repo-context.ts | 2 +- .../solve-issue/roles/committer/index.ts | 4 ++-- .../solve-issue/roles/implement/index.ts | 23 +++++++++++-------- workflows/solve-issue/roles/plan/index.ts | 23 +++++++++++-------- workflows/solve-issue/roles/prepare/index.ts | 4 ++-- workflows/solve-issue/roles/publish/index.ts | 23 +++++++++++-------- .../solve-issue/roles/read-issue/index.ts | 4 ++-- workflows/solve-issue/roles/review/index.ts | 6 ++--- workflows/solve-issue/roles/test/index.ts | 4 ++-- 15 files changed, 89 insertions(+), 64 deletions(-) diff --git a/workflows/knowledge-extraction/index.ts b/workflows/knowledge-extraction/index.ts index 1812f03..335eecb 100644 --- a/workflows/knowledge-extraction/index.ts +++ b/workflows/knowledge-extraction/index.ts @@ -20,7 +20,7 @@ const workflow = createKnowledgeExtractionWorkflow({ adapters: { explorer: createCursorAdapter({ type: "cursor", - model: "auto", + model: "claude-sonnet-4", timeout: CURSOR_TIMEOUT_MS, }), }, diff --git a/workflows/knowledge-extraction/lib/workdir.ts b/workflows/knowledge-extraction/lib/workdir.ts index 28bef95..08f56f5 100644 --- a/workflows/knowledge-extraction/lib/workdir.ts +++ b/workflows/knowledge-extraction/lib/workdir.ts @@ -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(); } diff --git a/workflows/knowledge-extraction/moderator.ts b/workflows/knowledge-extraction/moderator.ts index 019e162..8e04715 100644 --- a/workflows/knowledge-extraction/moderator.ts +++ b/workflows/knowledge-extraction/moderator.ts @@ -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["steps"]; +type Steps = ThreadContext["steps"]; function lastQuestionerRemaining(steps: Steps): QuestionerMeta | undefined { for (let i = steps.length - 1; i >= 0; i--) { diff --git a/workflows/knowledge-extraction/roles/answerer.ts b/workflows/knowledge-extraction/roles/answerer.ts index fdf93cd..1efca45 100644 --- a/workflows/knowledge-extraction/roles/answerer.ts +++ b/workflows/knowledge-extraction/roles/answerer.ts @@ -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 { 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).dryRun) { blocks.push(`### ${q.id}\n[dryRun] skipped nerve knowledge query\n`); continue; } @@ -93,7 +94,7 @@ export function createAnswererRole(deps: CreateAnswererRoleDeps): Role(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(messages, "questioner"); const am = lastMeta(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 { return createRole( adapter, - async (innerStart: StartStep, msgs: WorkflowMessage[]) => explorerPrompt(innerStart, msgs), + async (ctx: ThreadContext) => explorerPrompt(ctx), explorerMetaSchema, extract, ); diff --git a/workflows/knowledge-extraction/roles/questioner.ts b/workflows/knowledge-extraction/roles/questioner.ts index 8d2261c..5a09d70 100644 --- a/workflows/knowledge-extraction/roles/questioner.ts +++ b/workflows/knowledge-extraction/roles/questioner.ts @@ -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 { 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: { diff --git a/workflows/solve-issue/lib/repo-context.ts b/workflows/solve-issue/lib/repo-context.ts index b555c47..c0928a1 100644 --- a/workflows/solve-issue/lib/repo-context.ts +++ b/workflows/solve-issue/lib/repo-context.ts @@ -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; diff --git a/workflows/solve-issue/roles/committer/index.ts b/workflows/solve-issue/roles/committer/index.ts index 4f70a9a..44ec142 100644 --- a/workflows/solve-issue/roles/committer/index.ts +++ b/workflows/solve-issue/roles/committer/index.ts @@ -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 { const inner = createRole( adapter, - async (start: StartStep) => committerPrompt({ threadId: start.meta.threadId }), + async (ctx: ThreadContext) => committerPrompt({ threadId: ctx.start.meta.threadId }), committerMetaSchema, extract, ); diff --git a/workflows/solve-issue/roles/implement/index.ts b/workflows/solve-issue/roles/implement/index.ts index 7b386d9..6839dac 100644 --- a/workflows/solve-issue/roles/implement/index.ts +++ b/workflows/solve-issue/roles/implement/index.ts @@ -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 { - return async (start: StartStep, messages: WorkflowMessage[]): Promise> => { + return async (ctx: ThreadContext): Promise> => { + 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 { diff --git a/workflows/solve-issue/roles/plan/index.ts b/workflows/solve-issue/roles/plan/index.ts index 3520760..b99b0f4 100644 --- a/workflows/solve-issue/roles/plan/index.ts +++ b/workflows/solve-issue/roles/plan/index.ts @@ -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 { - return async (start: StartStep, messages: WorkflowMessage[]): Promise> => { + return async (ctx: ThreadContext): Promise> => { + 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 { diff --git a/workflows/solve-issue/roles/prepare/index.ts b/workflows/solve-issue/roles/prepare/index.ts index 97c8eca..e18eed7 100644 --- a/workflows/solve-issue/roles/prepare/index.ts +++ b/workflows/solve-issue/roles/prepare/index.ts @@ -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; export function createPrepareRole(adapter: AgentFn, extract: LlmExtractorConfig): Role { return createRole( adapter, - async (start: StartStep) => preparePrompt({ threadId: start.meta.threadId }), + async (ctx: ThreadContext) => preparePrompt({ threadId: ctx.start.meta.threadId }), prepareMetaSchema, extract, ); diff --git a/workflows/solve-issue/roles/publish/index.ts b/workflows/solve-issue/roles/publish/index.ts index ddaa10b..33053e6 100644 --- a/workflows/solve-issue/roles/publish/index.ts +++ b/workflows/solve-issue/roles/publish/index.ts @@ -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 { 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> => { + return async (ctx: ThreadContext): Promise> => { 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`; diff --git a/workflows/solve-issue/roles/read-issue/index.ts b/workflows/solve-issue/roles/read-issue/index.ts index fd58933..46377f9 100644 --- a/workflows/solve-issue/roles/read-issue/index.ts +++ b/workflows/solve-issue/roles/read-issue/index.ts @@ -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; export function createReadIssueRole(adapter: AgentFn, extract: LlmExtractorConfig): Role { return createRole( adapter, - async (start: StartStep) => readIssuePrompt({ threadId: start.meta.threadId }), + async (ctx: ThreadContext) => readIssuePrompt({ threadId: ctx.start.meta.threadId }), readIssueMetaSchema, extract, ); diff --git a/workflows/solve-issue/roles/review/index.ts b/workflows/solve-issue/roles/review/index.ts index 7908463..9f3cc42 100644 --- a/workflows/solve-issue/roles/review/index.ts +++ b/workflows/solve-issue/roles/review/index.ts @@ -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 { return createRole( adapter, - async (start: StartStep) => - reviewPrompt({ threadId: start.meta.threadId, nerveRoot }), + async (ctx: ThreadContext) => + reviewPrompt({ threadId: ctx.start.meta.threadId, nerveRoot }), reviewMetaSchema, extract, ); diff --git a/workflows/solve-issue/roles/test/index.ts b/workflows/solve-issue/roles/test/index.ts index 0a2edab..802de1e 100644 --- a/workflows/solve-issue/roles/test/index.ts +++ b/workflows/solve-issue/roles/test/index.ts @@ -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; export function createTestRole(adapter: AgentFn, extract: LlmExtractorConfig): Role { return createRole( adapter, - async (start: StartStep) => testPrompt({ threadId: start.meta.threadId }), + async (ctx: ThreadContext) => testPrompt({ threadId: ctx.start.meta.threadId }), testMetaSchema, extract, );