From 2482fb7e620daeafdac14ac44af6a4d8e8d8c07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Wed, 6 May 2026 14:25:44 +0000 Subject: [PATCH] chore: remove all dryRun infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dryRun no longer needed — tests use mock agents + mock fetch instead. Removes isDryRun from WorkflowFnOptions, dryRun from ExtractConfig, dryRunMeta from RoleDefinition, --dry-run from CLI, and all related plumbing in engine/worker/fork/extract. 小橘 --- docs/rfc-001-workflow-engine.md | 7 +- examples/hello-world.ts | 2 - .../cli-workflow/__tests__/fork-cli.test.ts | 6 +- .../cli-workflow/__tests__/thread-cli.test.ts | 12 +-- packages/cli-workflow/src/cli-dispatch.ts | 3 +- packages/cli-workflow/src/cmd-run.ts | 3 +- packages/cli-workflow/src/run-argv.ts | 17 +--- packages/workflow-role-coder/src/coder.ts | 5 -- .../__tests__/committer.test.ts | 8 +- .../workflow-role-committer/src/committer.ts | 5 -- packages/workflow-role-planner/src/planner.ts | 3 - .../__tests__/reviewer.test.ts | 4 +- .../workflow-role-reviewer/src/reviewer.ts | 1 - .../__tests__/solve-issue-template.test.ts | 86 +++++++++++++++++-- .../__tests__/build-descriptor.test.ts | 1 - packages/workflow/__tests__/engine.test.ts | 75 ++++++++++++++-- .../workflow/__tests__/fork-thread.test.ts | 4 +- .../__tests__/thread-jsonl-format.test.ts | 1 - packages/workflow/__tests__/worker.test.ts | 4 +- packages/workflow/src/create-workflow.ts | 2 - packages/workflow/src/engine.ts | 3 - packages/workflow/src/extract-meta.ts | 4 +- packages/workflow/src/fork-thread.ts | 11 +-- packages/workflow/src/llm-extract.ts | 7 -- packages/workflow/src/types.ts | 5 +- packages/workflow/src/worker.ts | 7 +- 26 files changed, 184 insertions(+), 102 deletions(-) diff --git a/docs/rfc-001-workflow-engine.md b/docs/rfc-001-workflow-engine.md index 49f4762..23700b5 100644 --- a/docs/rfc-001-workflow-engine.md +++ b/docs/rfc-001-workflow-engine.md @@ -44,7 +44,7 @@ type ThreadInput = { /** The bundle contract — an AsyncGenerator, not a Promise. */ type WorkflowFn = ( input: ThreadInput, - options: { isDryRun: boolean; maxRounds: number } + options: { threadId: string; maxRounds: number } ) => AsyncGenerator; ``` @@ -95,7 +95,7 @@ When using the `createRoleModerator` helper, fork is **naturally handled**: // It sees planner already ran → routes to coder automatically const gen = workflow( { prompt: "fix bug #3", steps: [{ role: "planner", content: "...", meta: {} }] }, - { isDryRun: false, maxRounds: 10 } + { threadId: "01KQXKW18CT8G75T53R8F4G7YG", maxRounds: 10 } ); // First yield will be coder's output, not planner's ``` @@ -225,7 +225,6 @@ No concurrency control or timeout settings in the registry — those belong to e "parameters": { "prompt": "Fix the login redirect bug in #3", "options": { - "isDryRun": false, "maxRounds": 5 } }, @@ -271,7 +270,7 @@ No concurrency control or timeout settings in the registry — those belong to e | `uncaged-workflow list` | List registered workflows | | `uncaged-workflow show ` | Show workflow details | | `uncaged-workflow remove ` | Remove a workflow | -| `uncaged-workflow run [--prompt] [--dry-run] [--max-rounds]` | Start a thread | +| `uncaged-workflow run [--prompt] [--max-rounds]` | Start a thread | | `uncaged-workflow threads [name]` | List threads (optionally filter by workflow) | | `uncaged-workflow thread ` | Show thread state | | `uncaged-workflow thread rm ` | Delete a thread | diff --git a/examples/hello-world.ts b/examples/hello-world.ts index e6c6ae5..1abe3a4 100644 --- a/examples/hello-world.ts +++ b/examples/hello-world.ts @@ -27,12 +27,10 @@ const greeter: RoleDefinition = { description: "Generates a greeting", systemPrompt: "You greet the user briefly.", schema: greeterMetaSchema, - dryRunMeta: { greeting: "Hello!" }, }; const extract = { provider: { baseUrl: "http://127.0.0.1:9", apiKey: "", model: "" }, - dryRun: true, } as const; export const run = createWorkflow( diff --git a/packages/cli-workflow/__tests__/fork-cli.test.ts b/packages/cli-workflow/__tests__/fork-cli.test.ts index 154398f..59c3285 100644 --- a/packages/cli-workflow/__tests__/fork-cli.test.ts +++ b/packages/cli-workflow/__tests__/fork-cli.test.ts @@ -98,7 +98,7 @@ describe("cli fork", () => { } const hash = added.value.hash; - const ran = await cmdRun(storageRoot, "solve-issue", "hello", false, 5); + const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); expect(ran.ok).toBe(true); if (!ran.ok) { return; @@ -148,7 +148,7 @@ describe("cli fork", () => { } const hash = added.value.hash; - const ran = await cmdRun(storageRoot, "solve-issue", "hello", false, 5); + const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); expect(ran.ok).toBe(true); if (!ran.ok) { return; @@ -198,7 +198,7 @@ describe("cli fork", () => { return; } - const ran = await cmdRun(storageRoot, "solve-issue", "hello", false, 5); + const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); expect(ran.ok).toBe(true); if (!ran.ok) { return; diff --git a/packages/cli-workflow/__tests__/thread-cli.test.ts b/packages/cli-workflow/__tests__/thread-cli.test.ts index 95b8184..dbc8f3f 100644 --- a/packages/cli-workflow/__tests__/thread-cli.test.ts +++ b/packages/cli-workflow/__tests__/thread-cli.test.ts @@ -138,7 +138,7 @@ describe("cli thread commands", () => { return; } - const ran = await cmdRun(storageRoot, "solve-issue", "hello", false, 5); + const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); expect(ran.ok).toBe(true); if (!ran.ok) { return; @@ -199,7 +199,7 @@ describe("cli thread commands", () => { return; } - const ran = await cmdRun(storageRoot, "solve-issue", "hello", false, 5); + const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); expect(ran.ok).toBe(true); if (!ran.ok) { return; @@ -229,7 +229,7 @@ describe("cli thread commands", () => { return; } - const ran = await cmdRun(storageRoot, "solve-issue", "hello", false, 5); + const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); expect(ran.ok).toBe(true); if (!ran.ok) { return; @@ -268,7 +268,7 @@ describe("cli thread commands", () => { return; } - const ran = await cmdRun(storageRoot, "solve-issue", "hello", false, 5); + const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); expect(ran.ok).toBe(true); if (!ran.ok) { return; @@ -309,7 +309,7 @@ describe("cli thread commands", () => { return; } - const ran = await cmdRun(storageRoot, "solve-issue", "hello", false, 5); + const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); expect(ran.ok).toBe(true); if (!ran.ok) { return; @@ -338,7 +338,7 @@ describe("cli thread commands", () => { return; } - const ran = await cmdRun(storageRoot, "solve-issue", "hello", false, 5); + const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); expect(ran.ok).toBe(true); if (!ran.ok) { return; diff --git a/packages/cli-workflow/src/cli-dispatch.ts b/packages/cli-workflow/src/cli-dispatch.ts index baf106e..2032eac 100644 --- a/packages/cli-workflow/src/cli-dispatch.ts +++ b/packages/cli-workflow/src/cli-dispatch.ts @@ -22,7 +22,7 @@ function usage(): string { " uncaged-workflow list", " uncaged-workflow show ", " uncaged-workflow remove ", - " uncaged-workflow run [--prompt ] [--dry-run] [--max-rounds N]", + " uncaged-workflow run [--prompt ] [--max-rounds N]", " uncaged-workflow ps", " uncaged-workflow kill ", " uncaged-workflow history ", @@ -111,7 +111,6 @@ async function dispatchRun(storageRoot: string, argv: string[]): Promise storageRoot, parsed.value.name, parsed.value.prompt, - parsed.value.dryRun, parsed.value.maxRounds, ); if (!result.ok) { diff --git a/packages/cli-workflow/src/cmd-run.ts b/packages/cli-workflow/src/cmd-run.ts index 82d985f..ace7e13 100644 --- a/packages/cli-workflow/src/cmd-run.ts +++ b/packages/cli-workflow/src/cmd-run.ts @@ -15,7 +15,6 @@ export async function cmdRun( storageRoot: string, name: string, prompt: string, - isDryRun: boolean, maxRounds: number, ): Promise> { const nameOk = validateCliWorkflowName(name); @@ -47,7 +46,7 @@ export async function cmdRun( threadId, workflowName: name, prompt, - options: { isDryRun, maxRounds }, + options: { maxRounds }, }, { awaitResponseLine: false }, ); diff --git a/packages/cli-workflow/src/run-argv.ts b/packages/cli-workflow/src/run-argv.ts index 21276e7..1107cfe 100644 --- a/packages/cli-workflow/src/run-argv.ts +++ b/packages/cli-workflow/src/run-argv.ts @@ -3,20 +3,13 @@ import { err, ok, type Result } from "@uncaged/workflow"; export type ParsedRunArgv = { name: string; prompt: string; - dryRun: boolean; maxRounds: number; }; -type FlagOk = - | { kind: "dry-run" } - | { kind: "prompt"; value: string } - | { kind: "max-rounds"; value: number }; +type FlagOk = { kind: "prompt"; value: string } | { kind: "max-rounds"; value: number }; function parseFlagAt(argv: string[], index: number): Result | null { const flag = argv[index]; - if (flag === "--dry-run") { - return ok({ kind: "dry-run" }); - } if (flag === "--prompt") { const value = argv[index + 1]; if (value === undefined) { @@ -41,7 +34,6 @@ function parseFlagAt(argv: string[], index: number): Result | nu export function parseRunArgv(argv: string[]): Result { let name: string | undefined; let prompt = ""; - let dryRun = false; let maxRounds = 5; let i = 0; @@ -62,11 +54,6 @@ export function parseRunArgv(argv: string[]): Result { } const flag = parsed.value; - if (flag.kind === "dry-run") { - dryRun = true; - i += 1; - continue; - } if (flag.kind === "prompt") { prompt = flag.value; i += 2; @@ -80,5 +67,5 @@ export function parseRunArgv(argv: string[]): Result { return err("run requires "); } - return ok({ name, prompt, dryRun, maxRounds }); + return ok({ name, prompt, maxRounds }); } diff --git a/packages/workflow-role-coder/src/coder.ts b/packages/workflow-role-coder/src/coder.ts index 3c17f2d..651c7ab 100644 --- a/packages/workflow-role-coder/src/coder.ts +++ b/packages/workflow-role-coder/src/coder.ts @@ -17,9 +17,4 @@ export const coderRole: RoleDefinition = { "Implements the next incomplete planner phase and reports structured completion metadata.", systemPrompt: CODER_SYSTEM, schema: coderMetaSchema, - dryRunMeta: { - completedPhase: "phase-1", - filesChanged: [], - summary: "", - }, }; diff --git a/packages/workflow-role-committer/__tests__/committer.test.ts b/packages/workflow-role-committer/__tests__/committer.test.ts index 36f63fe..dedfe79 100644 --- a/packages/workflow-role-committer/__tests__/committer.test.ts +++ b/packages/workflow-role-committer/__tests__/committer.test.ts @@ -3,8 +3,12 @@ import { describe, expect, test } from "bun:test"; import { committerMetaSchema, committerRole } from "../src/committer.js"; describe("committerRole", () => { - test("dryRunMeta validates against schema", () => { - const parsed = committerMetaSchema.safeParse(committerRole.dryRunMeta); + test("committed sample validates against schema", () => { + const parsed = committerMetaSchema.safeParse({ + status: "committed" as const, + branch: "feat/example", + commitSha: "abc1234", + }); expect(parsed.success).toBe(true); }); diff --git a/packages/workflow-role-committer/src/committer.ts b/packages/workflow-role-committer/src/committer.ts index 06778ed..e921fa6 100644 --- a/packages/workflow-role-committer/src/committer.ts +++ b/packages/workflow-role-committer/src/committer.ts @@ -29,9 +29,4 @@ export const committerRole: RoleDefinition = { description: "Creates branch, commits, and pushes when review passes.", systemPrompt: COMMITTER_SYSTEM, schema: committerMetaSchema, - dryRunMeta: { - status: "committed", - branch: "dry-run/placeholder", - commitSha: "0000000", - }, }; diff --git a/packages/workflow-role-planner/src/planner.ts b/packages/workflow-role-planner/src/planner.ts index cb7bb87..2cb7a82 100644 --- a/packages/workflow-role-planner/src/planner.ts +++ b/packages/workflow-role-planner/src/planner.ts @@ -23,7 +23,4 @@ export const plannerRole: RoleDefinition = { description: "Breaks the task into sequential phases for the coder.", systemPrompt: PLANNER_SYSTEM, schema: plannerMetaSchema, - dryRunMeta: { - phases: [{ name: "phase-1", description: "placeholder", acceptance: "placeholder" }], - }, }; diff --git a/packages/workflow-role-reviewer/__tests__/reviewer.test.ts b/packages/workflow-role-reviewer/__tests__/reviewer.test.ts index beb6fcf..96551ec 100644 --- a/packages/workflow-role-reviewer/__tests__/reviewer.test.ts +++ b/packages/workflow-role-reviewer/__tests__/reviewer.test.ts @@ -3,8 +3,8 @@ import { describe, expect, test } from "bun:test"; import { reviewerMetaSchema, reviewerRole } from "../src/reviewer.js"; describe("reviewerRole", () => { - test("dryRunMeta validates against schema", () => { - const parsed = reviewerMetaSchema.safeParse(reviewerRole.dryRunMeta); + test("approved sample validates against schema", () => { + const parsed = reviewerMetaSchema.safeParse({ status: "approved" as const }); expect(parsed.success).toBe(true); }); diff --git a/packages/workflow-role-reviewer/src/reviewer.ts b/packages/workflow-role-reviewer/src/reviewer.ts index 15fd3ce..5feb5f9 100644 --- a/packages/workflow-role-reviewer/src/reviewer.ts +++ b/packages/workflow-role-reviewer/src/reviewer.ts @@ -19,5 +19,4 @@ export const reviewerRole: RoleDefinition = { description: "Runs git diff checks and sets approved when the change is ready.", systemPrompt: REVIEWER_SYSTEM, schema: reviewerMetaSchema, - dryRunMeta: { status: "approved" }, }; diff --git a/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts b/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts index d46bcfc..a35ea1b 100644 --- a/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts +++ b/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; import { END, type RoleStep, @@ -7,16 +7,78 @@ import { validateWorkflowDescriptor, } from "@uncaged/workflow"; +import type { CoderMeta } from "@uncaged/workflow-role-coder"; import type { PlannerMeta } from "@uncaged/workflow-role-planner"; import { buildSolveIssueDescriptor } from "../src/descriptor.js"; -import { createSolveIssueRun, plannerRole, solveIssueModerator } from "../src/index.js"; +import { createSolveIssueRun, solveIssueModerator } from "../src/index.js"; import type { SolveIssueMeta } from "../src/roles.js"; const DEFAULT_PHASES: PlannerMeta["phases"] = [ { name: "phase-a", description: "Do the work", acceptance: "Done" }, ]; +const EXPECT_PLANNER_META: PlannerMeta = { + phases: [{ name: "phase-1", description: "placeholder", acceptance: "placeholder" }], +}; + +const EXPECT_CODER_META: CoderMeta = { + completedPhase: "phase-1", + filesChanged: [], + summary: "", +}; + +function installMockChatCompletions(sequence: ReadonlyArray>): () => void { + const origFetch = globalThis.fetch; + let i = 0; + const mockFetch = async ( + input: Parameters[0], + init?: RequestInit, + ): Promise => { + const args = sequence[i] ?? sequence[sequence.length - 1]; + if (args === undefined) { + throw new Error("installMockChatCompletions: empty sequence"); + } + i += 1; + void input; + const body = init?.body ? (JSON.parse(String(init.body)) as Record) : {}; + const tools = body.tools; + const firstTool = + Array.isArray(tools) && tools.length > 0 && tools[0] !== null && typeof tools[0] === "object" + ? (tools[0] as Record) + : null; + const fn = + firstTool !== null ? (firstTool.function as Record | undefined) : undefined; + const toolName = typeof fn?.name === "string" ? fn.name : "extract"; + return new Response( + JSON.stringify({ + choices: [ + { + message: { + tool_calls: [ + { + type: "function", + function: { + name: toolName, + arguments: JSON.stringify(args), + }, + }, + ], + }, + }, + ], + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + }; + globalThis.fetch = Object.assign(mockFetch, { + preconnect: origFetch.preconnect.bind(origFetch), + }) as typeof fetch; + return () => { + globalThis.fetch = origFetch; + }; +} + function makeStart(maxRounds: number): ThreadContext["start"] { return { role: START, @@ -78,7 +140,6 @@ function committerStep(): RoleStep { const stubExtract = { provider: { baseUrl: "http://127.0.0.1:9", apiKey: "", model: "test" }, - dryRun: true, } as const; describe("solveIssueModerator", () => { @@ -137,11 +198,20 @@ describe("solveIssueModerator", () => { }); describe("createSolveIssueRun", () => { - test("dry-run extraction yields role dryRunMeta for planner", async () => { + let restoreFetch: (() => void) | null = null; + + afterEach(() => { + restoreFetch?.(); + restoreFetch = null; + }); + + test("structured extraction yields planner meta from mocked chat completions", async () => { + restoreFetch = installMockChatCompletions([EXPECT_PLANNER_META]); + const run = createSolveIssueRun({ agent: async () => "" }, stubExtract); const gen = run( { prompt: "task", steps: [] }, - { threadId: "01TEST000000000000000000TR", isDryRun: true, maxRounds: 20 }, + { threadId: "01TEST000000000000000000TR", maxRounds: 20 }, ); const first = await gen.next(); expect(first.done).toBe(false); @@ -149,10 +219,12 @@ describe("createSolveIssueRun", () => { throw new Error("expected yield"); } expect(first.value.role).toBe("planner"); - expect(first.value.meta).toEqual(plannerRole.dryRunMeta); + expect(first.value.meta).toEqual(EXPECT_PLANNER_META); }); test("per-role agent overrides default", async () => { + restoreFetch = installMockChatCompletions([EXPECT_PLANNER_META, EXPECT_CODER_META]); + const calls: string[] = []; const run = createSolveIssueRun( { @@ -175,7 +247,7 @@ describe("createSolveIssueRun", () => { ); const gen = run( { prompt: "task", steps: [] }, - { threadId: "01TEST000000000000000000TR", isDryRun: true, maxRounds: 20 }, + { threadId: "01TEST000000000000000000TR", maxRounds: 20 }, ); await gen.next(); expect(calls).toEqual(["planner"]); diff --git a/packages/workflow/__tests__/build-descriptor.test.ts b/packages/workflow/__tests__/build-descriptor.test.ts index a6e5c55..6e1bb78 100644 --- a/packages/workflow/__tests__/build-descriptor.test.ts +++ b/packages/workflow/__tests__/build-descriptor.test.ts @@ -21,7 +21,6 @@ describe("buildDescriptor", () => { description: "Analyzes input", systemPrompt: "You are an analyst.", schema, - dryRunMeta: { title: "", count: 0 }, }, }, moderator: () => END, diff --git a/packages/workflow/__tests__/engine.test.ts b/packages/workflow/__tests__/engine.test.ts index fdfc03c..cea2876 100644 --- a/packages/workflow/__tests__/engine.test.ts +++ b/packages/workflow/__tests__/engine.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -23,9 +23,59 @@ type DemoMeta = { coder: z.infer; }; +function installMockChatCompletions(sequence: ReadonlyArray>): () => void { + const origFetch = globalThis.fetch; + let i = 0; + const mockFetch = async ( + input: Parameters[0], + init?: RequestInit, + ): Promise => { + const args = sequence[i] ?? sequence[sequence.length - 1]; + if (args === undefined) { + throw new Error("installMockChatCompletions: empty sequence"); + } + i += 1; + void input; + const body = init?.body ? (JSON.parse(String(init.body)) as Record) : {}; + const tools = body.tools; + const firstTool = + Array.isArray(tools) && tools.length > 0 && tools[0] !== null && typeof tools[0] === "object" + ? (tools[0] as Record) + : null; + const fn = + firstTool !== null ? (firstTool.function as Record | undefined) : undefined; + const toolName = typeof fn?.name === "string" ? fn.name : "extract"; + return new Response( + JSON.stringify({ + choices: [ + { + message: { + tool_calls: [ + { + type: "function", + function: { + name: toolName, + arguments: JSON.stringify(args), + }, + }, + ], + }, + }, + ], + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + }; + globalThis.fetch = Object.assign(mockFetch, { + preconnect: origFetch.preconnect.bind(origFetch), + }) as typeof fetch; + return () => { + globalThis.fetch = origFetch; + }; +} + const demoExtract = { provider: { baseUrl: "http://127.0.0.1:9", apiKey: "test", model: "test" }, - dryRun: true, } as const; const demoWorkflow = createWorkflow( @@ -35,13 +85,11 @@ const demoWorkflow = createWorkflow( description: "Demo planner", systemPrompt: "You are a planner.", schema: plannerMetaSchema, - dryRunMeta: { plan: "do-it", files: ["a.ts"] }, }, coder: { description: "Demo coder", systemPrompt: "You are a coder.", schema: coderMetaSchema, - dryRunMeta: { diff: "+ok" }, }, }, moderator: (ctx) => { @@ -65,7 +113,19 @@ const demoWorkflow = createWorkflow( ); describe("executeThread", () => { + let restoreFetch: (() => void) | null = null; + + afterEach(() => { + restoreFetch?.(); + restoreFetch = null; + }); + test("writes RFC-001 `.data.jsonl` start + role records and `.info.jsonl` logs", async () => { + restoreFetch = installMockChatCompletions([ + { plan: "do-it", files: ["a.ts"] }, + { diff: "+ok" }, + ]); + const root = await mkdtemp(join(tmpdir(), "wf-engine-")); try { const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; @@ -82,7 +142,6 @@ describe("executeThread", () => { "demo-flow", { prompt: "Fix the login redirect bug in #3", steps: [] }, { - isDryRun: false, maxRounds: 5, signal: ac.signal, awaitAfterEachYield: async () => {}, @@ -111,8 +170,8 @@ describe("executeThread", () => { const params = start.parameters as Record; expect(params.prompt).toBe("Fix the login redirect bug in #3"); const opts = params.options as Record; - expect(opts.isDryRun).toBe(false); expect(opts.maxRounds).toBe(5); + expect(Object.keys(opts).sort()).toEqual(["maxRounds"]); const role1 = JSON.parse(lines[1] ?? "{}") as Record; expect(role1.role).toBe("planner"); @@ -140,6 +199,8 @@ describe("executeThread", () => { }); test("pre-filled ThreadInput.steps skips roles already present", async () => { + restoreFetch = installMockChatCompletions([{ diff: "+ok" }]); + const root = await mkdtemp(join(tmpdir(), "wf-engine-fork-")); try { const threadId = "01KQXKW18CT8G75T53R8F4G7YG"; @@ -166,7 +227,6 @@ describe("executeThread", () => { ], }, { - isDryRun: false, maxRounds: 5, signal: ac.signal, awaitAfterEachYield: async () => {}, @@ -225,7 +285,6 @@ describe("executeThread", () => { "demo-flow", { prompt: "hello", steps: [] }, { - isDryRun: false, maxRounds: 0, signal: ac.signal, awaitAfterEachYield: async () => {}, diff --git a/packages/workflow/__tests__/fork-thread.test.ts b/packages/workflow/__tests__/fork-thread.test.ts index 25a140b..1d8ccb1 100644 --- a/packages/workflow/__tests__/fork-thread.test.ts +++ b/packages/workflow/__tests__/fork-thread.test.ts @@ -6,7 +6,7 @@ import { selectForkHistoricalSteps, } from "../src/fork-thread.js"; -const sampleDataJsonl = `{"name":"demo","hash":"C9NMV6V2TQT81","threadId":"01AAA1111111111111111111","parameters":{"prompt":"hi","options":{"isDryRun":false,"maxRounds":5}},"timestamp":100} +const sampleDataJsonl = `{"name":"demo","hash":"C9NMV6V2TQT81","threadId":"01AAA1111111111111111111","parameters":{"prompt":"hi","options":{"maxRounds":5}},"timestamp":100} {"role":"planner","content":"p","meta":{},"timestamp":101} {"role":"coder","content":"c","meta":{},"timestamp":102} {"role":"reviewer","content":"r","meta":{},"timestamp":103} @@ -23,6 +23,7 @@ describe("fork-thread", () => { expect(r.value.start.hash).toBe("C9NMV6V2TQT81"); expect(r.value.start.threadId).toBe("01AAA1111111111111111111"); expect(r.value.start.prompt).toBe("hi"); + expect(r.value.start.maxRounds).toBe(5); expect(r.value.roleSteps.length).toBe(3); expect(r.value.roleSteps[0]?.role).toBe("planner"); }); @@ -82,5 +83,6 @@ describe("fork-thread", () => { expect(r.value.workflowName).toBe("demo"); expect(r.value.historicalSteps.length).toBe(1); expect(r.value.historicalSteps[0]?.timestamp).toBe(101); + expect(r.value.runOptions).toEqual({ maxRounds: 5 }); }); }); diff --git a/packages/workflow/__tests__/thread-jsonl-format.test.ts b/packages/workflow/__tests__/thread-jsonl-format.test.ts index 05bfce5..25f8851 100644 --- a/packages/workflow/__tests__/thread-jsonl-format.test.ts +++ b/packages/workflow/__tests__/thread-jsonl-format.test.ts @@ -9,7 +9,6 @@ describe("RFC-001 thread JSONL shapes", () => { parameters: { prompt: "Fix the login redirect bug in #3", options: { - isDryRun: false, maxRounds: 5, }, }, diff --git a/packages/workflow/__tests__/worker.test.ts b/packages/workflow/__tests__/worker.test.ts index bf86ebc..6fdc1f5 100644 --- a/packages/workflow/__tests__/worker.test.ts +++ b/packages/workflow/__tests__/worker.test.ts @@ -102,7 +102,7 @@ describe("worker process", () => { threadId, workflowName: "demo-flow", prompt: "hello", - options: { isDryRun: false, maxRounds: 5 }, + options: { maxRounds: 5 }, }); const exitCode: number = await new Promise((resolve) => { @@ -150,7 +150,7 @@ describe("worker process", () => { threadId, workflowName: "demo-flow", prompt: "hello", - options: { isDryRun: false, maxRounds: 5 }, + options: { maxRounds: 5 }, steps: [ { role: "planner", diff --git a/packages/workflow/src/create-workflow.ts b/packages/workflow/src/create-workflow.ts index 8632bfc..f59be99 100644 --- a/packages/workflow/src/create-workflow.ts +++ b/packages/workflow/src/create-workflow.ts @@ -116,8 +116,6 @@ export function createWorkflow( const meta = await extractMetaOrThrow(next, raw, roleDef.schema, { provider: extract.provider, - dryRun: extract.dryRun, - dryRunMeta: roleDef.dryRunMeta, }); const ts = Date.now(); diff --git a/packages/workflow/src/engine.ts b/packages/workflow/src/engine.ts index 0182620..626e18b 100644 --- a/packages/workflow/src/engine.ts +++ b/packages/workflow/src/engine.ts @@ -20,7 +20,6 @@ export type PrefilledDiskStep = { }; export type ExecuteThreadOptions = { - isDryRun: boolean; maxRounds: number; signal: AbortSignal; /** Invoked after each successful yield (and outer-loop checks); used for pause/resume. */ @@ -133,7 +132,6 @@ export async function executeThread( parameters: { prompt: input.prompt, options: { - isDryRun: options.isDryRun, maxRounds: options.maxRounds, }, }, @@ -168,7 +166,6 @@ export async function executeThread( const bundleOptions: WorkflowFnOptions = { threadId: io.threadId, - isDryRun: options.isDryRun, maxRounds: options.maxRounds, }; diff --git a/packages/workflow/src/extract-meta.ts b/packages/workflow/src/extract-meta.ts index 62db1a8..6376645 100644 --- a/packages/workflow/src/extract-meta.ts +++ b/packages/workflow/src/extract-meta.ts @@ -7,14 +7,12 @@ export async function extractMetaOrThrow>( roleName: string, raw: string, schema: z.ZodType, - options: { provider: LlmProvider; dryRun: boolean; dryRunMeta: T }, + options: { provider: LlmProvider }, ): Promise { const result = await llmExtractWithRetry({ text: raw, schema, provider: options.provider, - dryRun: options.dryRun, - dryRunMeta: options.dryRunMeta, }); if (!result.ok) { throw new Error( diff --git a/packages/workflow/src/fork-thread.ts b/packages/workflow/src/fork-thread.ts index 4447eae..d2d67f4 100644 --- a/packages/workflow/src/fork-thread.ts +++ b/packages/workflow/src/fork-thread.ts @@ -9,7 +9,6 @@ export type ParsedThreadStartRecord = { hash: string; threadId: string; prompt: string; - isDryRun: boolean; maxRounds: number; }; @@ -72,10 +71,9 @@ function parseStartRecordLine(firstLine: string): Result; - const isDryRun = optRec.isDryRun; const maxRounds = optRec.maxRounds; - if (typeof isDryRun !== "boolean" || typeof maxRounds !== "number") { - return err("start record missing parameters.options.isDryRun or maxRounds"); + if (typeof maxRounds !== "number") { + return err("start record missing parameters.options.maxRounds"); } return ok({ @@ -83,7 +81,6 @@ function parseStartRecordLine(firstLine: string): Result = { text: string; schema: z.ZodType; provider: LlmProvider; - dryRun: boolean; - /** Returned when `dryRun` is true (ignored for live extract). */ - dryRunMeta: T; }; export type LlmError = @@ -127,10 +124,6 @@ export function llmErrorToCause(error: LlmError): Error { async function performLlmExtract( options: LlmExtractArgs & { userContent: string }, ): Promise> { - if (options.dryRun) { - return ok(options.dryRunMeta); - } - const rawJsonSchema = z.toJSONSchema(options.schema) as Record; const parameters = stripJsonSchemaMeta(rawJsonSchema); const toolName = readToolName(parameters); diff --git a/packages/workflow/src/types.ts b/packages/workflow/src/types.ts index bb3f5d9..4e8c4bd 100644 --- a/packages/workflow/src/types.ts +++ b/packages/workflow/src/types.ts @@ -36,7 +36,6 @@ export type ThreadInput = { /** Options passed to a workflow bundle's `run` export (engine-provided). */ export type WorkflowFnOptions = { threadId: string; - isDryRun: boolean; maxRounds: number; }; @@ -82,15 +81,13 @@ export type AgentBinding = { /** Structured extraction settings for the workflow engine. */ export type ExtractConfig = { provider: LlmProvider; - dryRun: boolean; }; -/** Role wiring: prompts, schema, dry-run meta, and human-readable description. */ +/** Role wiring: prompts, schema, and human-readable description. */ export type RoleDefinition> = { description: string; systemPrompt: string; schema: z.ZodType; - dryRunMeta: Meta; }; /** diff --git a/packages/workflow/src/worker.ts b/packages/workflow/src/worker.ts index 0261184..a985ebf 100644 --- a/packages/workflow/src/worker.ts +++ b/packages/workflow/src/worker.ts @@ -16,7 +16,7 @@ type RunCommand = { threadId: string; workflowName: string; prompt: string; - options: { isDryRun: boolean; maxRounds: number }; + options: { maxRounds: number }; steps: RoleOutput[]; /** Timestamps aligned with `steps` for `.data.jsonl` replay; length must match `steps` when non-null. */ stepTimestamps: number[] | null; @@ -114,9 +114,8 @@ function parseRunControlPayload(rec: Record): RunCommand | null return null; } const optRec = options as Record; - const isDryRun = optRec.isDryRun; const maxRounds = optRec.maxRounds; - if (typeof isDryRun !== "boolean" || typeof maxRounds !== "number") { + if (typeof maxRounds !== "number") { return null; } const parsedSteps = parseRunStepsPayload(rec); @@ -136,7 +135,7 @@ function parseRunControlPayload(rec: Record): RunCommand | null threadId, workflowName, prompt, - options: { isDryRun, maxRounds }, + options: { maxRounds }, steps: parsedSteps.steps, stepTimestamps: parsedSteps.stepTimestamps, forkSourceThreadId,