Merge pull request 'refactor: replace maxRounds with supervisor check interval' (#186) from refactor/185-remove-max-rounds into main

This commit is contained in:
2026-05-11 09:01:24 +00:00
32 changed files with 88 additions and 217 deletions
@@ -34,7 +34,6 @@ function noLogger(): (tag: string, content: string) => void {
function makeOptions(overrides: Partial<ExecuteThreadOptions>): ExecuteThreadOptions {
return {
maxRounds: 5,
depth: 0,
signal: new AbortController().signal,
awaitAfterEachYield: async () => {},
@@ -107,7 +106,7 @@ describe("executeThread (Phase 2 — CAS thread storage)", () => {
wf,
"demo",
{ prompt: "hello", steps: [] },
makeOptions({ storageRoot, maxRounds: 5 }),
makeOptions({ storageRoot }),
io,
noLogger(),
);
@@ -127,7 +126,6 @@ describe("executeThread (Phase 2 — CAS thread storage)", () => {
expect(startNode.type).toBe("start");
expect((startNode.payload as Record<string, unknown>).name).toBe("demo");
expect((startNode.payload as Record<string, unknown>).hash).toBe(bundleHash);
expect((startNode.payload as Record<string, unknown>).maxRounds).toBe(5);
const refs = startNode.refs as string[];
expect(refs.length).toBe(1);
@@ -164,7 +162,6 @@ describe("executeThread (Phase 2 — CAS thread storage)", () => {
const opts = makeOptions({
storageRoot,
maxRounds: 5,
awaitAfterEachYield: async () => {
const text = await readFile(join(bundleDir, "threads.json"), "utf8");
const parsed = JSON.parse(text) as Record<string, { head: string }>;
@@ -228,7 +225,7 @@ describe("executeThread (Phase 2 — CAS thread storage)", () => {
wf,
"demo",
{ prompt: "p", steps: [] },
makeOptions({ storageRoot, maxRounds: 5 }),
makeOptions({ storageRoot }),
io,
noLogger(),
);
@@ -279,7 +276,7 @@ describe("executeThread (Phase 2 — CAS thread storage)", () => {
wf,
"demo",
{ prompt: "p", steps: [] },
makeOptions({ storageRoot, maxRounds: 5 }),
makeOptions({ storageRoot }),
io,
noLogger(),
);
@@ -45,7 +45,6 @@ describe("garbageCollectCas (mark-and-sweep)", () => {
{
name: "demo",
hash: bundleHash,
maxRounds: 5,
depth: 0,
},
promptHash,
+2 -33
View File
@@ -284,21 +284,6 @@ async function driveWorkflowGenerator(params: {
});
}
if (written >= executeOptions.maxRounds) {
logger("R3CW7YBQ", `thread ${threadId} stopped at maxRounds=${executeOptions.maxRounds}`);
return await finalizeThread({
cas,
bundleDir,
threadId,
startHash,
chain,
completion: {
returnCode: 0,
summary: `completed: reached maxRounds (${executeOptions.maxRounds})`,
},
});
}
const iterResult = await gen.next();
if (iterResult.done) {
@@ -383,7 +368,7 @@ async function driveWorkflowGenerator(params: {
* Persistence layout (RFC v3 — CAS-based thread storage):
* - Thread chain is written as immutable CAS blobs: a single {@link StartNode}
* plus one {@link StateNode} per role step (including a final `__end__`
* state on completion / abort / `maxRounds`).
* state on completion / abort).
* - The active thread head is published in `<bundleDir>/threads.json`; on
* completion it is removed and a record is appended to
* `<bundleDir>/history/{YYYY-MM-DD}.jsonl`.
@@ -433,7 +418,6 @@ export async function executeThread(
{
name: workflowName,
hash: io.hash,
maxRounds: options.maxRounds,
depth: options.depth,
},
promptHash,
@@ -475,21 +459,6 @@ export async function executeThread(
const nowMs = Date.now();
if (options.maxRounds <= 0) {
logger("R3CW7YBQ", `thread ${io.threadId} stopped at maxRounds=${options.maxRounds}`);
return await finalizeThread({
cas: io.cas,
bundleDir,
threadId: io.threadId,
startHash,
chain,
completion: {
returnCode: 0,
summary: `completed: reached maxRounds (${options.maxRounds})`,
},
});
}
const registryRuntime = await resolveEngineRegistryRuntime(options.storageRoot, io.cas);
if (!registryRuntime.ok) {
throw new Error(registryRuntime.error);
@@ -501,7 +470,7 @@ export async function executeThread(
start: {
role: START,
content: input.prompt,
meta: { maxRounds: options.maxRounds },
meta: {},
timestamp: nowMs,
},
steps: input.steps.map((out, i) => ({
@@ -104,9 +104,7 @@ async function readPromptText(cas: CasStore, promptHash: string): Promise<Result
async function readStartWorkflowIdentity(params: {
cas: CasStore;
startHash: string;
}): Promise<
Result<{ workflowName: string; maxRounds: number; depth: number; prompt: string }, string>
> {
}): Promise<Result<{ workflowName: string; depth: number; prompt: string }, string>> {
const yamlText = await params.cas.get(params.startHash);
if (yamlText === null) {
return err(`start node missing in CAS: ${params.startHash}`);
@@ -127,7 +125,6 @@ async function readStartWorkflowIdentity(params: {
const p = parsed.node.payload;
return ok({
workflowName: p.name,
maxRounds: p.maxRounds,
depth: p.depth,
prompt: prompt.value,
});
@@ -317,7 +314,7 @@ export async function prepareCasFork(params: {
hash: params.bundleHash,
sourceThreadId: params.sourceThreadId,
prompt: id.value.prompt,
runOptions: { maxRounds: id.value.maxRounds, depth: id.value.depth },
runOptions: { depth: id.value.depth },
steps,
stepTimestamps,
forkContinuation: cont.value,
@@ -39,7 +39,6 @@ export type PrefilledDiskStep = {
};
export type ExecuteThreadOptions = {
maxRounds: number;
/** Passed to the bundle thread context as `ThreadContext.depth`. */
depth: number;
signal: AbortSignal;
@@ -68,7 +67,7 @@ export type CasForkPlan = {
hash: string;
sourceThreadId: string;
prompt: string;
runOptions: { maxRounds: number; depth: number };
runOptions: { depth: number };
steps: RoleOutput[];
stepTimestamps: number[];
forkContinuation: ForkContinuationOptions;
@@ -32,7 +32,7 @@ type RunCommand = {
threadId: string;
workflowName: string;
prompt: string;
options: { maxRounds: number; depth: number };
options: { depth: number };
steps: RoleOutput[];
/** Timestamps aligned with `steps` for replay / fork restore; length must match `steps` when steps are non-empty. */
stepTimestamps: number[] | null;
@@ -185,10 +185,6 @@ function parseRunControlPayload(rec: Record<string, unknown>): RunCommand | null
return null;
}
const optRec = options as Record<string, unknown>;
const maxRounds = optRec.maxRounds;
if (typeof maxRounds !== "number") {
return null;
}
const depthRaw = optRec.depth;
const depth =
typeof depthRaw === "number" && Number.isFinite(depthRaw) ? Math.trunc(depthRaw) : 0;
@@ -210,7 +206,7 @@ function parseRunControlPayload(rec: Record<string, unknown>): RunCommand | null
threadId,
workflowName,
prompt,
options: { maxRounds, depth },
options: { depth },
steps: parsedSteps.steps,
stepTimestamps: parsedSteps.stepTimestamps,
forkSourceThreadId,
@@ -1,10 +1,6 @@
import { type CasStore, getContentMerklePayload } from "@uncaged/workflow-cas";
import { createLlmFn, createThreadReactor } from "@uncaged/workflow-reactor";
import type {
ExtractFn,
ExtractResult,
LlmProvider,
} from "@uncaged/workflow-runtime";
import type { ExtractFn, ExtractResult, LlmProvider } from "@uncaged/workflow-runtime";
import type * as z from "zod/v4";
import { extractFunctionToolFromZodSchema } from "./llm-extract.js";
@@ -95,7 +95,6 @@ export function workflowAsAgent(
workflowName,
input,
{
maxRounds: ctx.start.meta.maxRounds,
depth: nextDepth,
signal: signalNever.signal,
awaitAfterEachYield: async () => {},