docs(rfc-001): WorkflowFn input → ThreadInput for fork/resume support

- First param is now { prompt, steps } instead of bare prompt
- steps: [] for new thread, pre-filled for fork/resume
- createRoleModerator naturally handles resume via moderator routing
- No special replay logic needed

小橘 <xiaoju@shazhou.work>
This commit is contained in:
2026-05-06 05:25:00 +00:00
parent eda00d1c8e
commit 9a4cec2b2d
14 changed files with 279 additions and 168 deletions
@@ -35,21 +35,11 @@ describe("cli workflow commands", () => {
bundlePath,
`import fs from "node:fs";
export default {
name: "solve-issue",
roles: {
noop: async () => {
fs.existsSync(".");
return { content: "ok", meta: { done: true } };
},
},
moderator(ctx) {
if (ctx.steps.length === 0) {
return "noop";
}
return "__end__";
},
};
export default async function* () {
fs.existsSync(".");
yield { role: "noop", content: "ok", meta: { done: true } };
return { returnCode: 0, summary: "done" };
}
`,
"utf8",
);
@@ -91,7 +81,7 @@ export default {
const bundlePath = join(storageRoot, "bad.esm.js");
await writeFile(
bundlePath,
'import x from "./local";\nexport default async function run() { return { returnCode: 0, summary: "" }; }\n',
'import x from "./local";\nexport default async function* run() { return { returnCode: 0, summary: "" }; }\n',
"utf8",
);
const r = await cmdAdd(storageRoot, "solve-issue", bundlePath);
@@ -13,54 +13,29 @@ import { cmdThreadRemove, cmdThreadShow } from "../src/cmd-thread.js";
import { cmdThreads } from "../src/cmd-threads.js";
import { pathExists } from "../src/fs-utils.js";
const fastBundleSource = `export default {
name: "solve-issue",
roles: {
planner: async () => ({ content: "plan", meta: { plan: "x" } }),
coder: async () => ({ content: "code", meta: { diff: "y" } }),
},
moderator(ctx) {
if (ctx.steps.length === 0) return "planner";
if (ctx.steps.length === 1) return "coder";
return "__end__";
},
};
const fastBundleSource = `export default async function* () {
yield { role: "planner", content: "plan", meta: { plan: "x" } };
yield { role: "coder", content: "code", meta: { diff: "y" } };
return { returnCode: 0, summary: "done" };
}
`;
const slowPlannerBundleSource = `export default {
name: "solve-issue",
roles: {
planner: async () => {
await new Promise((r) => setTimeout(r, 400));
return { content: "plan", meta: { plan: "x" } };
},
coder: async () => ({ content: "code", meta: { diff: "y" } }),
},
moderator(ctx) {
if (ctx.steps.length === 0) return "planner";
if (ctx.steps.length === 1) return "coder";
return "__end__";
},
};
const slowPlannerBundleSource = `export default async function* () {
await new Promise((r) => setTimeout(r, 400));
yield { role: "planner", content: "plan", meta: { plan: "x" } };
yield { role: "coder", content: "code", meta: { diff: "y" } };
return { returnCode: 0, summary: "done" };
}
`;
const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
const abortablePlannerBundleSource = `export default {
name: "solve-issue",
roles: {
planner: async () => {
await new Promise((r) => setTimeout(r, 600));
return { content: "plan", meta: { plan: "x" } };
},
coder: async () => ({ content: "code", meta: { diff: "y" } }),
},
moderator(ctx) {
if (ctx.steps.length === 0) return "planner";
if (ctx.steps.length === 1) return "coder";
return "__end__";
},
};
const abortablePlannerBundleSource = `export default async function* () {
await new Promise((r) => setTimeout(r, 600));
yield { role: "planner", content: "plan", meta: { plan: "x" } };
yield { role: "coder", content: "code", meta: { diff: "y" } };
return { returnCode: 0, summary: "done" };
}
`;
describe("cli thread commands", () => {
+1
View File
@@ -43,6 +43,7 @@ export async function cmdRun(
const sent = await sendWorkerTcpCommand(worker.value.port, {
type: "run",
threadId,
workflowName: name,
prompt,
options: { isDryRun, maxRounds },
});