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. */
|
/** The bundle contract — an AsyncGenerator, not a Promise. */
|
||||||
type WorkflowFn = (
|
type WorkflowFn = (
|
||||||
input: ThreadInput,
|
input: ThreadInput,
|
||||||
options: { isDryRun: boolean; maxRounds: number }
|
options: { threadId: string; maxRounds: number }
|
||||||
) => AsyncGenerator<RoleOutput, WorkflowResult>;
|
) => 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
|
// It sees planner already ran → routes to coder automatically
|
||||||
const gen = workflow(
|
const gen = workflow(
|
||||||
{ prompt: "fix bug #3", steps: [{ role: "planner", content: "...", meta: {} }] },
|
{ 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
|
// 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": {
|
"parameters": {
|
||||||
"prompt": "Fix the login redirect bug in #3",
|
"prompt": "Fix the login redirect bug in #3",
|
||||||
"options": {
|
"options": {
|
||||||
"isDryRun": false,
|
|
||||||
"maxRounds": 5
|
"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 list` | List registered workflows |
|
||||||
| `uncaged-workflow show <name>` | Show workflow details |
|
| `uncaged-workflow show <name>` | Show workflow details |
|
||||||
| `uncaged-workflow remove <name>` | Remove a workflow |
|
| `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 threads [name]` | List threads (optionally filter by workflow) |
|
||||||
| `uncaged-workflow thread <id>` | Show thread state |
|
| `uncaged-workflow thread <id>` | Show thread state |
|
||||||
| `uncaged-workflow thread rm <id>` | Delete a thread |
|
| `uncaged-workflow thread rm <id>` | Delete a thread |
|
||||||
|
|||||||
@@ -27,12 +27,10 @@ const greeter: RoleDefinition<Roles["greeter"]> = {
|
|||||||
description: "Generates a greeting",
|
description: "Generates a greeting",
|
||||||
systemPrompt: "You greet the user briefly.",
|
systemPrompt: "You greet the user briefly.",
|
||||||
schema: greeterMetaSchema,
|
schema: greeterMetaSchema,
|
||||||
dryRunMeta: { greeting: "Hello!" },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const extract = {
|
const extract = {
|
||||||
provider: { baseUrl: "http://127.0.0.1:9", apiKey: "", model: "" },
|
provider: { baseUrl: "http://127.0.0.1:9", apiKey: "", model: "" },
|
||||||
dryRun: true,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const run = createWorkflow<Roles>(
|
export const run = createWorkflow<Roles>(
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ describe("cli fork", () => {
|
|||||||
}
|
}
|
||||||
const hash = added.value.hash;
|
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);
|
expect(ran.ok).toBe(true);
|
||||||
if (!ran.ok) {
|
if (!ran.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -148,7 +148,7 @@ describe("cli fork", () => {
|
|||||||
}
|
}
|
||||||
const hash = added.value.hash;
|
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);
|
expect(ran.ok).toBe(true);
|
||||||
if (!ran.ok) {
|
if (!ran.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -198,7 +198,7 @@ describe("cli fork", () => {
|
|||||||
return;
|
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);
|
expect(ran.ok).toBe(true);
|
||||||
if (!ran.ok) {
|
if (!ran.ok) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ describe("cli thread commands", () => {
|
|||||||
return;
|
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);
|
expect(ran.ok).toBe(true);
|
||||||
if (!ran.ok) {
|
if (!ran.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -199,7 +199,7 @@ describe("cli thread commands", () => {
|
|||||||
return;
|
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);
|
expect(ran.ok).toBe(true);
|
||||||
if (!ran.ok) {
|
if (!ran.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -229,7 +229,7 @@ describe("cli thread commands", () => {
|
|||||||
return;
|
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);
|
expect(ran.ok).toBe(true);
|
||||||
if (!ran.ok) {
|
if (!ran.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -268,7 +268,7 @@ describe("cli thread commands", () => {
|
|||||||
return;
|
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);
|
expect(ran.ok).toBe(true);
|
||||||
if (!ran.ok) {
|
if (!ran.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -309,7 +309,7 @@ describe("cli thread commands", () => {
|
|||||||
return;
|
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);
|
expect(ran.ok).toBe(true);
|
||||||
if (!ran.ok) {
|
if (!ran.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -338,7 +338,7 @@ describe("cli thread commands", () => {
|
|||||||
return;
|
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);
|
expect(ran.ok).toBe(true);
|
||||||
if (!ran.ok) {
|
if (!ran.ok) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function usage(): string {
|
|||||||
" uncaged-workflow list",
|
" uncaged-workflow list",
|
||||||
" uncaged-workflow show <name>",
|
" uncaged-workflow show <name>",
|
||||||
" uncaged-workflow remove <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 ps",
|
||||||
" uncaged-workflow kill <thread-id>",
|
" uncaged-workflow kill <thread-id>",
|
||||||
" uncaged-workflow history <name>",
|
" uncaged-workflow history <name>",
|
||||||
@@ -111,7 +111,6 @@ async function dispatchRun(storageRoot: string, argv: string[]): Promise<number>
|
|||||||
storageRoot,
|
storageRoot,
|
||||||
parsed.value.name,
|
parsed.value.name,
|
||||||
parsed.value.prompt,
|
parsed.value.prompt,
|
||||||
parsed.value.dryRun,
|
|
||||||
parsed.value.maxRounds,
|
parsed.value.maxRounds,
|
||||||
);
|
);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export async function cmdRun(
|
|||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
name: string,
|
name: string,
|
||||||
prompt: string,
|
prompt: string,
|
||||||
isDryRun: boolean,
|
|
||||||
maxRounds: number,
|
maxRounds: number,
|
||||||
): Promise<Result<{ threadId: string }, string>> {
|
): Promise<Result<{ threadId: string }, string>> {
|
||||||
const nameOk = validateCliWorkflowName(name);
|
const nameOk = validateCliWorkflowName(name);
|
||||||
@@ -47,7 +46,7 @@ export async function cmdRun(
|
|||||||
threadId,
|
threadId,
|
||||||
workflowName: name,
|
workflowName: name,
|
||||||
prompt,
|
prompt,
|
||||||
options: { isDryRun, maxRounds },
|
options: { maxRounds },
|
||||||
},
|
},
|
||||||
{ awaitResponseLine: false },
|
{ awaitResponseLine: false },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,20 +3,13 @@ import { err, ok, type Result } from "@uncaged/workflow";
|
|||||||
export type ParsedRunArgv = {
|
export type ParsedRunArgv = {
|
||||||
name: string;
|
name: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
dryRun: boolean;
|
|
||||||
maxRounds: number;
|
maxRounds: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FlagOk =
|
type FlagOk = { kind: "prompt"; value: string } | { kind: "max-rounds"; value: number };
|
||||||
| { kind: "dry-run" }
|
|
||||||
| { kind: "prompt"; value: string }
|
|
||||||
| { kind: "max-rounds"; value: number };
|
|
||||||
|
|
||||||
function parseFlagAt(argv: string[], index: number): Result<FlagOk, string> | null {
|
function parseFlagAt(argv: string[], index: number): Result<FlagOk, string> | null {
|
||||||
const flag = argv[index];
|
const flag = argv[index];
|
||||||
if (flag === "--dry-run") {
|
|
||||||
return ok({ kind: "dry-run" });
|
|
||||||
}
|
|
||||||
if (flag === "--prompt") {
|
if (flag === "--prompt") {
|
||||||
const value = argv[index + 1];
|
const value = argv[index + 1];
|
||||||
if (value === undefined) {
|
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> {
|
export function parseRunArgv(argv: string[]): Result<ParsedRunArgv, string> {
|
||||||
let name: string | undefined;
|
let name: string | undefined;
|
||||||
let prompt = "";
|
let prompt = "";
|
||||||
let dryRun = false;
|
|
||||||
let maxRounds = 5;
|
let maxRounds = 5;
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -62,11 +54,6 @@ export function parseRunArgv(argv: string[]): Result<ParsedRunArgv, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const flag = parsed.value;
|
const flag = parsed.value;
|
||||||
if (flag.kind === "dry-run") {
|
|
||||||
dryRun = true;
|
|
||||||
i += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (flag.kind === "prompt") {
|
if (flag.kind === "prompt") {
|
||||||
prompt = flag.value;
|
prompt = flag.value;
|
||||||
i += 2;
|
i += 2;
|
||||||
@@ -80,5 +67,5 @@ export function parseRunArgv(argv: string[]): Result<ParsedRunArgv, string> {
|
|||||||
return err("run requires <name>");
|
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.",
|
"Implements the next incomplete planner phase and reports structured completion metadata.",
|
||||||
systemPrompt: CODER_SYSTEM,
|
systemPrompt: CODER_SYSTEM,
|
||||||
schema: coderMetaSchema,
|
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";
|
import { committerMetaSchema, committerRole } from "../src/committer.js";
|
||||||
|
|
||||||
describe("committerRole", () => {
|
describe("committerRole", () => {
|
||||||
test("dryRunMeta validates against schema", () => {
|
test("committed sample validates against schema", () => {
|
||||||
const parsed = committerMetaSchema.safeParse(committerRole.dryRunMeta);
|
const parsed = committerMetaSchema.safeParse({
|
||||||
|
status: "committed" as const,
|
||||||
|
branch: "feat/example",
|
||||||
|
commitSha: "abc1234",
|
||||||
|
});
|
||||||
expect(parsed.success).toBe(true);
|
expect(parsed.success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,4 @@ export const committerRole: RoleDefinition<CommitterMeta> = {
|
|||||||
description: "Creates branch, commits, and pushes when review passes.",
|
description: "Creates branch, commits, and pushes when review passes.",
|
||||||
systemPrompt: COMMITTER_SYSTEM,
|
systemPrompt: COMMITTER_SYSTEM,
|
||||||
schema: committerMetaSchema,
|
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.",
|
description: "Breaks the task into sequential phases for the coder.",
|
||||||
systemPrompt: PLANNER_SYSTEM,
|
systemPrompt: PLANNER_SYSTEM,
|
||||||
schema: plannerMetaSchema,
|
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";
|
import { reviewerMetaSchema, reviewerRole } from "../src/reviewer.js";
|
||||||
|
|
||||||
describe("reviewerRole", () => {
|
describe("reviewerRole", () => {
|
||||||
test("dryRunMeta validates against schema", () => {
|
test("approved sample validates against schema", () => {
|
||||||
const parsed = reviewerMetaSchema.safeParse(reviewerRole.dryRunMeta);
|
const parsed = reviewerMetaSchema.safeParse({ status: "approved" as const });
|
||||||
expect(parsed.success).toBe(true);
|
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.",
|
description: "Runs git diff checks and sets approved when the change is ready.",
|
||||||
systemPrompt: REVIEWER_SYSTEM,
|
systemPrompt: REVIEWER_SYSTEM,
|
||||||
schema: reviewerMetaSchema,
|
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 {
|
import {
|
||||||
END,
|
END,
|
||||||
type RoleStep,
|
type RoleStep,
|
||||||
@@ -7,16 +7,78 @@ import {
|
|||||||
validateWorkflowDescriptor,
|
validateWorkflowDescriptor,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
|
|
||||||
|
import type { CoderMeta } from "@uncaged/workflow-role-coder";
|
||||||
import type { PlannerMeta } from "@uncaged/workflow-role-planner";
|
import type { PlannerMeta } from "@uncaged/workflow-role-planner";
|
||||||
|
|
||||||
import { buildSolveIssueDescriptor } from "../src/descriptor.js";
|
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";
|
import type { SolveIssueMeta } from "../src/roles.js";
|
||||||
|
|
||||||
const DEFAULT_PHASES: PlannerMeta["phases"] = [
|
const DEFAULT_PHASES: PlannerMeta["phases"] = [
|
||||||
{ name: "phase-a", description: "Do the work", acceptance: "Done" },
|
{ 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"] {
|
function makeStart(maxRounds: number): ThreadContext<SolveIssueMeta>["start"] {
|
||||||
return {
|
return {
|
||||||
role: START,
|
role: START,
|
||||||
@@ -78,7 +140,6 @@ function committerStep(): RoleStep<SolveIssueMeta> {
|
|||||||
|
|
||||||
const stubExtract = {
|
const stubExtract = {
|
||||||
provider: { baseUrl: "http://127.0.0.1:9", apiKey: "", model: "test" },
|
provider: { baseUrl: "http://127.0.0.1:9", apiKey: "", model: "test" },
|
||||||
dryRun: true,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
describe("solveIssueModerator", () => {
|
describe("solveIssueModerator", () => {
|
||||||
@@ -137,11 +198,20 @@ describe("solveIssueModerator", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("createSolveIssueRun", () => {
|
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 run = createSolveIssueRun({ agent: async () => "" }, stubExtract);
|
||||||
const gen = run(
|
const gen = run(
|
||||||
{ prompt: "task", steps: [] },
|
{ prompt: "task", steps: [] },
|
||||||
{ threadId: "01TEST000000000000000000TR", isDryRun: true, maxRounds: 20 },
|
{ threadId: "01TEST000000000000000000TR", maxRounds: 20 },
|
||||||
);
|
);
|
||||||
const first = await gen.next();
|
const first = await gen.next();
|
||||||
expect(first.done).toBe(false);
|
expect(first.done).toBe(false);
|
||||||
@@ -149,10 +219,12 @@ describe("createSolveIssueRun", () => {
|
|||||||
throw new Error("expected yield");
|
throw new Error("expected yield");
|
||||||
}
|
}
|
||||||
expect(first.value.role).toBe("planner");
|
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 () => {
|
test("per-role agent overrides default", async () => {
|
||||||
|
restoreFetch = installMockChatCompletions([EXPECT_PLANNER_META, EXPECT_CODER_META]);
|
||||||
|
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const run = createSolveIssueRun(
|
const run = createSolveIssueRun(
|
||||||
{
|
{
|
||||||
@@ -175,7 +247,7 @@ describe("createSolveIssueRun", () => {
|
|||||||
);
|
);
|
||||||
const gen = run(
|
const gen = run(
|
||||||
{ prompt: "task", steps: [] },
|
{ prompt: "task", steps: [] },
|
||||||
{ threadId: "01TEST000000000000000000TR", isDryRun: true, maxRounds: 20 },
|
{ threadId: "01TEST000000000000000000TR", maxRounds: 20 },
|
||||||
);
|
);
|
||||||
await gen.next();
|
await gen.next();
|
||||||
expect(calls).toEqual(["planner"]);
|
expect(calls).toEqual(["planner"]);
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ describe("buildDescriptor", () => {
|
|||||||
description: "Analyzes input",
|
description: "Analyzes input",
|
||||||
systemPrompt: "You are an analyst.",
|
systemPrompt: "You are an analyst.",
|
||||||
schema,
|
schema,
|
||||||
dryRunMeta: { title: "", count: 0 },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
moderator: () => END,
|
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 { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
@@ -23,9 +23,59 @@ type DemoMeta = {
|
|||||||
coder: z.infer<typeof coderMetaSchema>;
|
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 = {
|
const demoExtract = {
|
||||||
provider: { baseUrl: "http://127.0.0.1:9", apiKey: "test", model: "test" },
|
provider: { baseUrl: "http://127.0.0.1:9", apiKey: "test", model: "test" },
|
||||||
dryRun: true,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const demoWorkflow = createWorkflow<DemoMeta>(
|
const demoWorkflow = createWorkflow<DemoMeta>(
|
||||||
@@ -35,13 +85,11 @@ const demoWorkflow = createWorkflow<DemoMeta>(
|
|||||||
description: "Demo planner",
|
description: "Demo planner",
|
||||||
systemPrompt: "You are a planner.",
|
systemPrompt: "You are a planner.",
|
||||||
schema: plannerMetaSchema,
|
schema: plannerMetaSchema,
|
||||||
dryRunMeta: { plan: "do-it", files: ["a.ts"] },
|
|
||||||
},
|
},
|
||||||
coder: {
|
coder: {
|
||||||
description: "Demo coder",
|
description: "Demo coder",
|
||||||
systemPrompt: "You are a coder.",
|
systemPrompt: "You are a coder.",
|
||||||
schema: coderMetaSchema,
|
schema: coderMetaSchema,
|
||||||
dryRunMeta: { diff: "+ok" },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
moderator: (ctx) => {
|
moderator: (ctx) => {
|
||||||
@@ -65,7 +113,19 @@ const demoWorkflow = createWorkflow<DemoMeta>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
describe("executeThread", () => {
|
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 () => {
|
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-"));
|
const root = await mkdtemp(join(tmpdir(), "wf-engine-"));
|
||||||
try {
|
try {
|
||||||
const threadId = "01KQXKW18CT8G75T53R8F4G7YG";
|
const threadId = "01KQXKW18CT8G75T53R8F4G7YG";
|
||||||
@@ -82,7 +142,6 @@ describe("executeThread", () => {
|
|||||||
"demo-flow",
|
"demo-flow",
|
||||||
{ prompt: "Fix the login redirect bug in #3", steps: [] },
|
{ prompt: "Fix the login redirect bug in #3", steps: [] },
|
||||||
{
|
{
|
||||||
isDryRun: false,
|
|
||||||
maxRounds: 5,
|
maxRounds: 5,
|
||||||
signal: ac.signal,
|
signal: ac.signal,
|
||||||
awaitAfterEachYield: async () => {},
|
awaitAfterEachYield: async () => {},
|
||||||
@@ -111,8 +170,8 @@ describe("executeThread", () => {
|
|||||||
const params = start.parameters as Record<string, unknown>;
|
const params = start.parameters as Record<string, unknown>;
|
||||||
expect(params.prompt).toBe("Fix the login redirect bug in #3");
|
expect(params.prompt).toBe("Fix the login redirect bug in #3");
|
||||||
const opts = params.options as Record<string, unknown>;
|
const opts = params.options as Record<string, unknown>;
|
||||||
expect(opts.isDryRun).toBe(false);
|
|
||||||
expect(opts.maxRounds).toBe(5);
|
expect(opts.maxRounds).toBe(5);
|
||||||
|
expect(Object.keys(opts).sort()).toEqual(["maxRounds"]);
|
||||||
|
|
||||||
const role1 = JSON.parse(lines[1] ?? "{}") as Record<string, unknown>;
|
const role1 = JSON.parse(lines[1] ?? "{}") as Record<string, unknown>;
|
||||||
expect(role1.role).toBe("planner");
|
expect(role1.role).toBe("planner");
|
||||||
@@ -140,6 +199,8 @@ describe("executeThread", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("pre-filled ThreadInput.steps skips roles already present", async () => {
|
test("pre-filled ThreadInput.steps skips roles already present", async () => {
|
||||||
|
restoreFetch = installMockChatCompletions([{ diff: "+ok" }]);
|
||||||
|
|
||||||
const root = await mkdtemp(join(tmpdir(), "wf-engine-fork-"));
|
const root = await mkdtemp(join(tmpdir(), "wf-engine-fork-"));
|
||||||
try {
|
try {
|
||||||
const threadId = "01KQXKW18CT8G75T53R8F4G7YG";
|
const threadId = "01KQXKW18CT8G75T53R8F4G7YG";
|
||||||
@@ -166,7 +227,6 @@ describe("executeThread", () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isDryRun: false,
|
|
||||||
maxRounds: 5,
|
maxRounds: 5,
|
||||||
signal: ac.signal,
|
signal: ac.signal,
|
||||||
awaitAfterEachYield: async () => {},
|
awaitAfterEachYield: async () => {},
|
||||||
@@ -225,7 +285,6 @@ describe("executeThread", () => {
|
|||||||
"demo-flow",
|
"demo-flow",
|
||||||
{ prompt: "hello", steps: [] },
|
{ prompt: "hello", steps: [] },
|
||||||
{
|
{
|
||||||
isDryRun: false,
|
|
||||||
maxRounds: 0,
|
maxRounds: 0,
|
||||||
signal: ac.signal,
|
signal: ac.signal,
|
||||||
awaitAfterEachYield: async () => {},
|
awaitAfterEachYield: async () => {},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
selectForkHistoricalSteps,
|
selectForkHistoricalSteps,
|
||||||
} from "../src/fork-thread.js";
|
} 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":"planner","content":"p","meta":{},"timestamp":101}
|
||||||
{"role":"coder","content":"c","meta":{},"timestamp":102}
|
{"role":"coder","content":"c","meta":{},"timestamp":102}
|
||||||
{"role":"reviewer","content":"r","meta":{},"timestamp":103}
|
{"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.hash).toBe("C9NMV6V2TQT81");
|
||||||
expect(r.value.start.threadId).toBe("01AAA1111111111111111111");
|
expect(r.value.start.threadId).toBe("01AAA1111111111111111111");
|
||||||
expect(r.value.start.prompt).toBe("hi");
|
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.length).toBe(3);
|
||||||
expect(r.value.roleSteps[0]?.role).toBe("planner");
|
expect(r.value.roleSteps[0]?.role).toBe("planner");
|
||||||
});
|
});
|
||||||
@@ -82,5 +83,6 @@ describe("fork-thread", () => {
|
|||||||
expect(r.value.workflowName).toBe("demo");
|
expect(r.value.workflowName).toBe("demo");
|
||||||
expect(r.value.historicalSteps.length).toBe(1);
|
expect(r.value.historicalSteps.length).toBe(1);
|
||||||
expect(r.value.historicalSteps[0]?.timestamp).toBe(101);
|
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: {
|
parameters: {
|
||||||
prompt: "Fix the login redirect bug in #3",
|
prompt: "Fix the login redirect bug in #3",
|
||||||
options: {
|
options: {
|
||||||
isDryRun: false,
|
|
||||||
maxRounds: 5,
|
maxRounds: 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ describe("worker process", () => {
|
|||||||
threadId,
|
threadId,
|
||||||
workflowName: "demo-flow",
|
workflowName: "demo-flow",
|
||||||
prompt: "hello",
|
prompt: "hello",
|
||||||
options: { isDryRun: false, maxRounds: 5 },
|
options: { maxRounds: 5 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const exitCode: number = await new Promise((resolve) => {
|
const exitCode: number = await new Promise((resolve) => {
|
||||||
@@ -150,7 +150,7 @@ describe("worker process", () => {
|
|||||||
threadId,
|
threadId,
|
||||||
workflowName: "demo-flow",
|
workflowName: "demo-flow",
|
||||||
prompt: "hello",
|
prompt: "hello",
|
||||||
options: { isDryRun: false, maxRounds: 5 },
|
options: { maxRounds: 5 },
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
role: "planner",
|
role: "planner",
|
||||||
|
|||||||
@@ -116,8 +116,6 @@ export function createWorkflow<M extends RoleMeta>(
|
|||||||
|
|
||||||
const meta = await extractMetaOrThrow(next, raw, roleDef.schema, {
|
const meta = await extractMetaOrThrow(next, raw, roleDef.schema, {
|
||||||
provider: extract.provider,
|
provider: extract.provider,
|
||||||
dryRun: extract.dryRun,
|
|
||||||
dryRunMeta: roleDef.dryRunMeta,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const ts = Date.now();
|
const ts = Date.now();
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export type PrefilledDiskStep = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ExecuteThreadOptions = {
|
export type ExecuteThreadOptions = {
|
||||||
isDryRun: boolean;
|
|
||||||
maxRounds: number;
|
maxRounds: number;
|
||||||
signal: AbortSignal;
|
signal: AbortSignal;
|
||||||
/** Invoked after each successful yield (and outer-loop checks); used for pause/resume. */
|
/** Invoked after each successful yield (and outer-loop checks); used for pause/resume. */
|
||||||
@@ -133,7 +132,6 @@ export async function executeThread(
|
|||||||
parameters: {
|
parameters: {
|
||||||
prompt: input.prompt,
|
prompt: input.prompt,
|
||||||
options: {
|
options: {
|
||||||
isDryRun: options.isDryRun,
|
|
||||||
maxRounds: options.maxRounds,
|
maxRounds: options.maxRounds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -168,7 +166,6 @@ export async function executeThread(
|
|||||||
|
|
||||||
const bundleOptions: WorkflowFnOptions = {
|
const bundleOptions: WorkflowFnOptions = {
|
||||||
threadId: io.threadId,
|
threadId: io.threadId,
|
||||||
isDryRun: options.isDryRun,
|
|
||||||
maxRounds: options.maxRounds,
|
maxRounds: options.maxRounds,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,12 @@ export async function extractMetaOrThrow<T extends Record<string, unknown>>(
|
|||||||
roleName: string,
|
roleName: string,
|
||||||
raw: string,
|
raw: string,
|
||||||
schema: z.ZodType<T>,
|
schema: z.ZodType<T>,
|
||||||
options: { provider: LlmProvider; dryRun: boolean; dryRunMeta: T },
|
options: { provider: LlmProvider },
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const result = await llmExtractWithRetry({
|
const result = await llmExtractWithRetry({
|
||||||
text: raw,
|
text: raw,
|
||||||
schema,
|
schema,
|
||||||
provider: options.provider,
|
provider: options.provider,
|
||||||
dryRun: options.dryRun,
|
|
||||||
dryRunMeta: options.dryRunMeta,
|
|
||||||
});
|
});
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export type ParsedThreadStartRecord = {
|
|||||||
hash: string;
|
hash: string;
|
||||||
threadId: string;
|
threadId: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
isDryRun: boolean;
|
|
||||||
maxRounds: number;
|
maxRounds: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,10 +71,9 @@ function parseStartRecordLine(firstLine: string): Result<ParsedThreadStartRecord
|
|||||||
return err("start record missing parameters.options");
|
return err("start record missing parameters.options");
|
||||||
}
|
}
|
||||||
const optRec = options as Record<string, unknown>;
|
const optRec = options as Record<string, unknown>;
|
||||||
const isDryRun = optRec.isDryRun;
|
|
||||||
const maxRounds = optRec.maxRounds;
|
const maxRounds = optRec.maxRounds;
|
||||||
if (typeof isDryRun !== "boolean" || typeof maxRounds !== "number") {
|
if (typeof maxRounds !== "number") {
|
||||||
return err("start record missing parameters.options.isDryRun or maxRounds");
|
return err("start record missing parameters.options.maxRounds");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok({
|
return ok({
|
||||||
@@ -83,7 +81,6 @@ function parseStartRecordLine(firstLine: string): Result<ParsedThreadStartRecord
|
|||||||
hash,
|
hash,
|
||||||
threadId,
|
threadId,
|
||||||
prompt,
|
prompt,
|
||||||
isDryRun,
|
|
||||||
maxRounds,
|
maxRounds,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -197,7 +194,7 @@ export type ForkPlan = {
|
|||||||
hash: string;
|
hash: string;
|
||||||
sourceThreadId: string;
|
sourceThreadId: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
runOptions: { isDryRun: boolean; maxRounds: number };
|
runOptions: { maxRounds: number };
|
||||||
historicalSteps: ForkHistoricalStep[];
|
historicalSteps: ForkHistoricalStep[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -222,7 +219,7 @@ export function buildForkPlan(
|
|||||||
hash: start.hash,
|
hash: start.hash,
|
||||||
sourceThreadId: start.threadId,
|
sourceThreadId: start.threadId,
|
||||||
prompt: start.prompt,
|
prompt: start.prompt,
|
||||||
runOptions: { isDryRun: start.isDryRun, maxRounds: start.maxRounds },
|
runOptions: { maxRounds: start.maxRounds },
|
||||||
historicalSteps: selected.value,
|
historicalSteps: selected.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ export type LlmExtractArgs<T> = {
|
|||||||
text: string;
|
text: string;
|
||||||
schema: z.ZodType<T>;
|
schema: z.ZodType<T>;
|
||||||
provider: LlmProvider;
|
provider: LlmProvider;
|
||||||
dryRun: boolean;
|
|
||||||
/** Returned when `dryRun` is true (ignored for live extract). */
|
|
||||||
dryRunMeta: T;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LlmError =
|
export type LlmError =
|
||||||
@@ -127,10 +124,6 @@ export function llmErrorToCause(error: LlmError): Error {
|
|||||||
async function performLlmExtract<T>(
|
async function performLlmExtract<T>(
|
||||||
options: LlmExtractArgs<T> & { userContent: string },
|
options: LlmExtractArgs<T> & { userContent: string },
|
||||||
): Promise<Result<T, LlmError>> {
|
): Promise<Result<T, LlmError>> {
|
||||||
if (options.dryRun) {
|
|
||||||
return ok(options.dryRunMeta);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawJsonSchema = z.toJSONSchema(options.schema) as Record<string, unknown>;
|
const rawJsonSchema = z.toJSONSchema(options.schema) as Record<string, unknown>;
|
||||||
const parameters = stripJsonSchemaMeta(rawJsonSchema);
|
const parameters = stripJsonSchemaMeta(rawJsonSchema);
|
||||||
const toolName = readToolName(parameters);
|
const toolName = readToolName(parameters);
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ export type ThreadInput = {
|
|||||||
/** Options passed to a workflow bundle's `run` export (engine-provided). */
|
/** Options passed to a workflow bundle's `run` export (engine-provided). */
|
||||||
export type WorkflowFnOptions = {
|
export type WorkflowFnOptions = {
|
||||||
threadId: string;
|
threadId: string;
|
||||||
isDryRun: boolean;
|
|
||||||
maxRounds: number;
|
maxRounds: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,15 +81,13 @@ export type AgentBinding = {
|
|||||||
/** Structured extraction settings for the workflow engine. */
|
/** Structured extraction settings for the workflow engine. */
|
||||||
export type ExtractConfig = {
|
export type ExtractConfig = {
|
||||||
provider: LlmProvider;
|
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>> = {
|
export type RoleDefinition<Meta extends Record<string, unknown>> = {
|
||||||
description: string;
|
description: string;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
schema: z.ZodType<Meta>;
|
schema: z.ZodType<Meta>;
|
||||||
dryRunMeta: Meta;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type RunCommand = {
|
|||||||
threadId: string;
|
threadId: string;
|
||||||
workflowName: string;
|
workflowName: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
options: { isDryRun: boolean; maxRounds: number };
|
options: { maxRounds: number };
|
||||||
steps: RoleOutput[];
|
steps: RoleOutput[];
|
||||||
/** Timestamps aligned with `steps` for `.data.jsonl` replay; length must match `steps` when non-null. */
|
/** Timestamps aligned with `steps` for `.data.jsonl` replay; length must match `steps` when non-null. */
|
||||||
stepTimestamps: number[] | null;
|
stepTimestamps: number[] | null;
|
||||||
@@ -114,9 +114,8 @@ function parseRunControlPayload(rec: Record<string, unknown>): RunCommand | null
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const optRec = options as Record<string, unknown>;
|
const optRec = options as Record<string, unknown>;
|
||||||
const isDryRun = optRec.isDryRun;
|
|
||||||
const maxRounds = optRec.maxRounds;
|
const maxRounds = optRec.maxRounds;
|
||||||
if (typeof isDryRun !== "boolean" || typeof maxRounds !== "number") {
|
if (typeof maxRounds !== "number") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const parsedSteps = parseRunStepsPayload(rec);
|
const parsedSteps = parseRunStepsPayload(rec);
|
||||||
@@ -136,7 +135,7 @@ function parseRunControlPayload(rec: Record<string, unknown>): RunCommand | null
|
|||||||
threadId,
|
threadId,
|
||||||
workflowName,
|
workflowName,
|
||||||
prompt,
|
prompt,
|
||||||
options: { isDryRun, maxRounds },
|
options: { maxRounds },
|
||||||
steps: parsedSteps.steps,
|
steps: parsedSteps.steps,
|
||||||
stepTimestamps: parsedSteps.stepTimestamps,
|
stepTimestamps: parsedSteps.stepTimestamps,
|
||||||
forkSourceThreadId,
|
forkSourceThreadId,
|
||||||
|
|||||||
Reference in New Issue
Block a user