chore: remove all dryRun infrastructure
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. 小橘 <xiaoju@shazhou.work>
This commit is contained in:
@@ -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<RoleOutput, WorkflowResult>;
|
||||
```
|
||||
|
||||
@@ -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 <name>` | Show workflow details |
|
||||
| `uncaged-workflow remove <name>` | Remove a workflow |
|
||||
| `uncaged-workflow run <name> [--prompt] [--dry-run] [--max-rounds]` | Start a thread |
|
||||
| `uncaged-workflow run <name> [--prompt] [--max-rounds]` | Start a thread |
|
||||
| `uncaged-workflow threads [name]` | List threads (optionally filter by workflow) |
|
||||
| `uncaged-workflow thread <id>` | Show thread state |
|
||||
| `uncaged-workflow thread rm <id>` | Delete a thread |
|
||||
|
||||
@@ -27,12 +27,10 @@ const greeter: RoleDefinition<Roles["greeter"]> = {
|
||||
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<Roles>(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -22,7 +22,7 @@ function usage(): string {
|
||||
" uncaged-workflow list",
|
||||
" uncaged-workflow show <name>",
|
||||
" uncaged-workflow remove <name>",
|
||||
" uncaged-workflow run <name> [--prompt <text>] [--dry-run] [--max-rounds N]",
|
||||
" uncaged-workflow run <name> [--prompt <text>] [--max-rounds N]",
|
||||
" uncaged-workflow ps",
|
||||
" uncaged-workflow kill <thread-id>",
|
||||
" uncaged-workflow history <name>",
|
||||
@@ -111,7 +111,6 @@ async function dispatchRun(storageRoot: string, argv: string[]): Promise<number>
|
||||
storageRoot,
|
||||
parsed.value.name,
|
||||
parsed.value.prompt,
|
||||
parsed.value.dryRun,
|
||||
parsed.value.maxRounds,
|
||||
);
|
||||
if (!result.ok) {
|
||||
|
||||
@@ -15,7 +15,6 @@ export async function cmdRun(
|
||||
storageRoot: string,
|
||||
name: string,
|
||||
prompt: string,
|
||||
isDryRun: boolean,
|
||||
maxRounds: number,
|
||||
): Promise<Result<{ threadId: string }, string>> {
|
||||
const nameOk = validateCliWorkflowName(name);
|
||||
@@ -47,7 +46,7 @@ export async function cmdRun(
|
||||
threadId,
|
||||
workflowName: name,
|
||||
prompt,
|
||||
options: { isDryRun, maxRounds },
|
||||
options: { maxRounds },
|
||||
},
|
||||
{ awaitResponseLine: false },
|
||||
);
|
||||
|
||||
@@ -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<FlagOk, string> | 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<FlagOk, string> | nu
|
||||
export function parseRunArgv(argv: string[]): Result<ParsedRunArgv, string> {
|
||||
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<ParsedRunArgv, string> {
|
||||
}
|
||||
|
||||
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<ParsedRunArgv, string> {
|
||||
return err("run requires <name>");
|
||||
}
|
||||
|
||||
return ok({ name, prompt, dryRun, maxRounds });
|
||||
return ok({ name, prompt, maxRounds });
|
||||
}
|
||||
|
||||
@@ -17,9 +17,4 @@ export const coderRole: RoleDefinition<CoderMeta> = {
|
||||
"Implements the next incomplete planner phase and reports structured completion metadata.",
|
||||
systemPrompt: CODER_SYSTEM,
|
||||
schema: coderMetaSchema,
|
||||
dryRunMeta: {
|
||||
completedPhase: "phase-1",
|
||||
filesChanged: [],
|
||||
summary: "",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -29,9 +29,4 @@ export const committerRole: RoleDefinition<CommitterMeta> = {
|
||||
description: "Creates branch, commits, and pushes when review passes.",
|
||||
systemPrompt: COMMITTER_SYSTEM,
|
||||
schema: committerMetaSchema,
|
||||
dryRunMeta: {
|
||||
status: "committed",
|
||||
branch: "dry-run/placeholder",
|
||||
commitSha: "0000000",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,7 +23,4 @@ export const plannerRole: RoleDefinition<PlannerMeta> = {
|
||||
description: "Breaks the task into sequential phases for the coder.",
|
||||
systemPrompt: PLANNER_SYSTEM,
|
||||
schema: plannerMetaSchema,
|
||||
dryRunMeta: {
|
||||
phases: [{ name: "phase-1", description: "placeholder", acceptance: "placeholder" }],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -19,5 +19,4 @@ export const reviewerRole: RoleDefinition<ReviewerMeta> = {
|
||||
description: "Runs git diff checks and sets approved when the change is ready.",
|
||||
systemPrompt: REVIEWER_SYSTEM,
|
||||
schema: reviewerMetaSchema,
|
||||
dryRunMeta: { status: "approved" },
|
||||
};
|
||||
|
||||
@@ -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<Record<string, unknown>>): () => void {
|
||||
const origFetch = globalThis.fetch;
|
||||
let i = 0;
|
||||
const mockFetch = async (
|
||||
input: Parameters<typeof fetch>[0],
|
||||
init?: RequestInit,
|
||||
): Promise<Response> => {
|
||||
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<string, unknown>) : {};
|
||||
const tools = body.tools;
|
||||
const firstTool =
|
||||
Array.isArray(tools) && tools.length > 0 && tools[0] !== null && typeof tools[0] === "object"
|
||||
? (tools[0] as Record<string, unknown>)
|
||||
: null;
|
||||
const fn =
|
||||
firstTool !== null ? (firstTool.function as Record<string, unknown> | 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<SolveIssueMeta>["start"] {
|
||||
return {
|
||||
role: START,
|
||||
@@ -78,7 +140,6 @@ function committerStep(): RoleStep<SolveIssueMeta> {
|
||||
|
||||
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"]);
|
||||
|
||||
@@ -21,7 +21,6 @@ describe("buildDescriptor", () => {
|
||||
description: "Analyzes input",
|
||||
systemPrompt: "You are an analyst.",
|
||||
schema,
|
||||
dryRunMeta: { title: "", count: 0 },
|
||||
},
|
||||
},
|
||||
moderator: () => END,
|
||||
|
||||
@@ -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<typeof coderMetaSchema>;
|
||||
};
|
||||
|
||||
function installMockChatCompletions(sequence: ReadonlyArray<Record<string, unknown>>): () => void {
|
||||
const origFetch = globalThis.fetch;
|
||||
let i = 0;
|
||||
const mockFetch = async (
|
||||
input: Parameters<typeof fetch>[0],
|
||||
init?: RequestInit,
|
||||
): Promise<Response> => {
|
||||
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<string, unknown>) : {};
|
||||
const tools = body.tools;
|
||||
const firstTool =
|
||||
Array.isArray(tools) && tools.length > 0 && tools[0] !== null && typeof tools[0] === "object"
|
||||
? (tools[0] as Record<string, unknown>)
|
||||
: null;
|
||||
const fn =
|
||||
firstTool !== null ? (firstTool.function as Record<string, unknown> | 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<DemoMeta>(
|
||||
@@ -35,13 +85,11 @@ const demoWorkflow = createWorkflow<DemoMeta>(
|
||||
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<DemoMeta>(
|
||||
);
|
||||
|
||||
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<string, unknown>;
|
||||
expect(params.prompt).toBe("Fix the login redirect bug in #3");
|
||||
const opts = params.options as Record<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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 () => {},
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ describe("RFC-001 thread JSONL shapes", () => {
|
||||
parameters: {
|
||||
prompt: "Fix the login redirect bug in #3",
|
||||
options: {
|
||||
isDryRun: false,
|
||||
maxRounds: 5,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -116,8 +116,6 @@ export function createWorkflow<M extends RoleMeta>(
|
||||
|
||||
const meta = await extractMetaOrThrow(next, raw, roleDef.schema, {
|
||||
provider: extract.provider,
|
||||
dryRun: extract.dryRun,
|
||||
dryRunMeta: roleDef.dryRunMeta,
|
||||
});
|
||||
|
||||
const ts = Date.now();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,14 +7,12 @@ export async function extractMetaOrThrow<T extends Record<string, unknown>>(
|
||||
roleName: string,
|
||||
raw: string,
|
||||
schema: z.ZodType<T>,
|
||||
options: { provider: LlmProvider; dryRun: boolean; dryRunMeta: T },
|
||||
options: { provider: LlmProvider },
|
||||
): Promise<T> {
|
||||
const result = await llmExtractWithRetry({
|
||||
text: raw,
|
||||
schema,
|
||||
provider: options.provider,
|
||||
dryRun: options.dryRun,
|
||||
dryRunMeta: options.dryRunMeta,
|
||||
});
|
||||
if (!result.ok) {
|
||||
throw new Error(
|
||||
|
||||
@@ -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<ParsedThreadStartRecord
|
||||
return err("start record missing parameters.options");
|
||||
}
|
||||
const optRec = options as Record<string, unknown>;
|
||||
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<ParsedThreadStartRecord
|
||||
hash,
|
||||
threadId,
|
||||
prompt,
|
||||
isDryRun,
|
||||
maxRounds,
|
||||
});
|
||||
}
|
||||
@@ -197,7 +194,7 @@ export type ForkPlan = {
|
||||
hash: string;
|
||||
sourceThreadId: string;
|
||||
prompt: string;
|
||||
runOptions: { isDryRun: boolean; maxRounds: number };
|
||||
runOptions: { maxRounds: number };
|
||||
historicalSteps: ForkHistoricalStep[];
|
||||
};
|
||||
|
||||
@@ -222,7 +219,7 @@ export function buildForkPlan(
|
||||
hash: start.hash,
|
||||
sourceThreadId: start.threadId,
|
||||
prompt: start.prompt,
|
||||
runOptions: { isDryRun: start.isDryRun, maxRounds: start.maxRounds },
|
||||
runOptions: { maxRounds: start.maxRounds },
|
||||
historicalSteps: selected.value,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@ export type LlmExtractArgs<T> = {
|
||||
text: string;
|
||||
schema: z.ZodType<T>;
|
||||
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<T>(
|
||||
options: LlmExtractArgs<T> & { userContent: string },
|
||||
): Promise<Result<T, LlmError>> {
|
||||
if (options.dryRun) {
|
||||
return ok(options.dryRunMeta);
|
||||
}
|
||||
|
||||
const rawJsonSchema = z.toJSONSchema(options.schema) as Record<string, unknown>;
|
||||
const parameters = stripJsonSchemaMeta(rawJsonSchema);
|
||||
const toolName = readToolName(parameters);
|
||||
|
||||
@@ -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<Meta extends Record<string, unknown>> = {
|
||||
description: string;
|
||||
systemPrompt: string;
|
||||
schema: z.ZodType<Meta>;
|
||||
dryRunMeta: Meta;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string, unknown>): RunCommand | null
|
||||
return null;
|
||||
}
|
||||
const optRec = options as Record<string, unknown>;
|
||||
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<string, unknown>): RunCommand | null
|
||||
threadId,
|
||||
workflowName,
|
||||
prompt,
|
||||
options: { isDryRun, maxRounds },
|
||||
options: { maxRounds },
|
||||
steps: parsedSteps.steps,
|
||||
stepTimestamps: parsedSteps.stepTimestamps,
|
||||
forkSourceThreadId,
|
||||
|
||||
Reference in New Issue
Block a user