@kontourai/flow-agents 3.8.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.9.0](https://github.com/kontourai/flow-agents/compare/v3.8.0...v3.9.0) (2026-07-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * **workflow:** add bounded continuation driver ([#560](https://github.com/kontourai/flow-agents/issues/560)) ([e6365ab](https://github.com/kontourai/flow-agents/commit/e6365aba324c76ee164681c987d20916c315444e))
9
+
3
10
  ## [3.8.0](https://github.com/kontourai/flow-agents/compare/v3.7.0...v3.8.0) (2026-07-12)
4
11
 
5
12
 
@@ -760,7 +760,9 @@ function projectFlowRun(context, run, sidecar) {
760
760
  const complete = run.state.status === "completed";
761
761
  const paused = run.state.status === "paused";
762
762
  const canceled = run.state.status === "canceled";
763
- const action = complete || paused || canceled ? { skills: [], operations: [] } : stepAction(run.definitionId, run.state.current_step);
763
+ const needsDecision = run.state.status === "needs_decision";
764
+ const failed = run.state.status === "failed";
765
+ const action = complete || paused || canceled || needsDecision || failed ? { skills: [], operations: [] } : stepAction(run.definitionId, run.state.current_step);
764
766
  if (!action) {
765
767
  throw new BuilderBuildRunInputError("kit.flow_step_actions", `does not declare Builder step ${run.state.current_step}`);
766
768
  }
@@ -785,18 +787,22 @@ function projectFlowRun(context, run, sidecar) {
785
787
  ? { status: "done", summary: "Canonical Flow run was canceled by an authorized external request. Artifacts are retained until separately archived." }
786
788
  : paused
787
789
  ? { status: "blocked", summary: "Canonical Flow run is paused. The current assignment actor may resume it with a reason." }
788
- : {
789
- status: "continue",
790
- summary: `Flow step \`${run.state.current_step}\`: ${skillText}${operationText} ${gateText}${routeText} Then synchronize the recorded evidence.`,
791
- skills,
792
- operations,
793
- command: syncCommand,
794
- };
790
+ : needsDecision
791
+ ? { status: "blocked", summary: "Canonical Flow requires an external decision before continuation." }
792
+ : failed
793
+ ? { status: "failed", summary: "Canonical Flow run failed; no continuation turn is allowed." }
794
+ : {
795
+ status: "continue",
796
+ summary: `Flow step \`${run.state.current_step}\`: ${skillText}${operationText} ${gateText}${routeText} Then synchronize the recorded evidence.`,
797
+ skills,
798
+ operations,
799
+ command: syncCommand,
800
+ };
795
801
  const phase = phaseForStep(definition.phase_map, run.state.current_step) ?? sidecar.phase;
796
802
  return {
797
803
  ...sidecar,
798
- status: complete ? "delivered" : canceled ? "canceled" : paused ? "blocked" : (run.state.transitions.length > 0 ? "in_progress" : sidecar.status),
799
- phase: complete || canceled ? "done" : phase,
804
+ status: complete ? "delivered" : canceled ? "canceled" : failed ? "failed" : (paused || needsDecision) ? "blocked" : (run.state.transitions.length > 0 ? "in_progress" : sidecar.status),
805
+ phase: complete || canceled || failed ? "done" : phase,
800
806
  updated_at: run.state.updated_at,
801
807
  flow_run: {
802
808
  run_id: run.runId,
@@ -0,0 +1,24 @@
1
+ import type { ContinuationBarrier, ContinuationTurnRequest, ContinuationTurnResult } from "../continuation-driver.js";
2
+ export type ContinuationAdapterCommand = {
3
+ argv: string[];
4
+ identity: string;
5
+ integrity: Array<{
6
+ file: string;
7
+ sha256: string;
8
+ }>;
9
+ };
10
+ export declare function loadContinuationAdapterCommand(commandFileInput: string): ContinuationAdapterCommand;
11
+ export declare function executeContinuationAdapter(commandFileInput: string, request: ContinuationTurnRequest, options: {
12
+ cwd: string;
13
+ timeoutMs: number;
14
+ }): Promise<ContinuationTurnResult>;
15
+ export declare function executeLoadedContinuationAdapter(command: ContinuationAdapterCommand, request: ContinuationTurnRequest, options: {
16
+ cwd: string;
17
+ timeoutMs: number;
18
+ }): Promise<ContinuationTurnResult>;
19
+ export declare function waitForContinuationBarrier(barrier: ContinuationBarrier, options: {
20
+ maxWaitMs: number;
21
+ pollMs: number;
22
+ now?: () => number;
23
+ sleep?: (ms: number) => Promise<void>;
24
+ }): Promise<"ready" | "pending">;
@@ -0,0 +1,225 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ export function loadContinuationAdapterCommand(commandFileInput) {
6
+ const commandFile = path.resolve(commandFileInput);
7
+ const command = validateAdapterCommand(JSON.parse(readRegularFileNoFollow(commandFile, "continuation adapter command file")));
8
+ if (!path.isAbsolute(command.argv[0]))
9
+ throw new Error("continuation adapter executable must be an absolute path");
10
+ const integrity = [...new Set(command.argv.filter((entry) => path.isAbsolute(entry) && regularFileExists(entry)))]
11
+ .map((file) => ({ file, sha256: sha256File(file, "continuation adapter integrity file") }));
12
+ if (!integrity.some((entry) => entry.file === command.argv[0]))
13
+ throw new Error("continuation adapter executable must be a regular file");
14
+ const identity = createHash("sha256").update(JSON.stringify({ ...command, integrity })).digest("hex");
15
+ return { ...command, identity, integrity };
16
+ }
17
+ export async function executeContinuationAdapter(commandFileInput, request, options) {
18
+ const command = loadContinuationAdapterCommand(commandFileInput);
19
+ return executeLoadedContinuationAdapter(command, request, options);
20
+ }
21
+ export async function executeLoadedContinuationAdapter(command, request, options) {
22
+ for (const entry of command.integrity) {
23
+ if (sha256File(entry.file, "continuation adapter integrity file") !== entry.sha256) {
24
+ throw new Error(`continuation adapter integrity changed after mission binding: ${entry.file}`);
25
+ }
26
+ }
27
+ assertPositiveInteger(options.timeoutMs, "continuation adapter timeoutMs", 1, 86_400_000);
28
+ return await spawnAdapter(command, request, options);
29
+ }
30
+ export async function waitForContinuationBarrier(barrier, options) {
31
+ assertPositiveInteger(options.maxWaitMs, "continuation barrier maxWaitMs", 0, 86_400_000);
32
+ assertPositiveInteger(options.pollMs, "continuation barrier pollMs", 1, 60_000);
33
+ const now = options.now ?? Date.now;
34
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
35
+ const stopAt = now() + options.maxWaitMs;
36
+ if (barrier.kind === "deadline") {
37
+ const deadline = Date.parse(barrier.at);
38
+ if (deadline <= now())
39
+ return "ready";
40
+ const waitMs = Math.min(deadline - now(), Math.max(0, stopAt - now()));
41
+ if (waitMs > 0)
42
+ await sleep(waitMs);
43
+ return Date.parse(barrier.at) <= now() ? "ready" : "pending";
44
+ }
45
+ while (pidAlive(barrier.pid)) {
46
+ const remaining = stopAt - now();
47
+ if (remaining <= 0)
48
+ return "pending";
49
+ await sleep(Math.min(options.pollMs, remaining));
50
+ }
51
+ return "ready";
52
+ }
53
+ function spawnAdapter(command, request, options) {
54
+ return new Promise((resolve, reject) => {
55
+ const child = spawn(command.argv[0], command.argv.slice(1), {
56
+ cwd: options.cwd,
57
+ env: process.env,
58
+ detached: process.platform !== "win32",
59
+ shell: false,
60
+ stdio: ["pipe", "pipe", "pipe"],
61
+ });
62
+ const stdout = [];
63
+ const stderr = [];
64
+ let stdoutBytes = 0;
65
+ let stderrBytes = 0;
66
+ let settled = false;
67
+ let forcedKill;
68
+ let terminationError;
69
+ const maxBytes = 4 * 1024 * 1024;
70
+ const timeout = setTimeout(() => {
71
+ if (settled)
72
+ return;
73
+ terminationError = new Error(`continuation adapter timed out after ${options.timeoutMs}ms`);
74
+ terminateProcessGroup(child.pid, "SIGTERM");
75
+ forcedKill = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 250);
76
+ }, options.timeoutMs);
77
+ child.stdout.on("data", (chunk) => {
78
+ stdoutBytes += chunk.length;
79
+ if (stdoutBytes > maxBytes) {
80
+ if (!terminationError) {
81
+ terminationError = new Error("continuation adapter stdout exceeded 4 MiB");
82
+ clearTimeout(timeout);
83
+ terminateProcessGroup(child.pid, "SIGTERM");
84
+ forcedKill = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 250);
85
+ }
86
+ return;
87
+ }
88
+ stdout.push(chunk);
89
+ });
90
+ child.stderr.on("data", (chunk) => {
91
+ stderrBytes += chunk.length;
92
+ if (stderrBytes <= maxBytes)
93
+ stderr.push(chunk);
94
+ });
95
+ child.on("error", (error) => {
96
+ if (settled)
97
+ return;
98
+ settled = true;
99
+ clearTimeout(timeout);
100
+ if (forcedKill)
101
+ clearTimeout(forcedKill);
102
+ reject(error);
103
+ });
104
+ child.on("close", (code, signal) => {
105
+ if (settled)
106
+ return;
107
+ settled = true;
108
+ clearTimeout(timeout);
109
+ if (forcedKill)
110
+ clearTimeout(forcedKill);
111
+ if (terminationError) {
112
+ reject(terminationError);
113
+ return;
114
+ }
115
+ const stderrText = Buffer.concat(stderr).toString("utf8").trim();
116
+ if (code !== 0) {
117
+ scheduleProcessGroupTermination(child.pid);
118
+ reject(new Error(`continuation adapter exited ${code ?? signal ?? "unknown"}${stderrText ? `: ${stderrText}` : ""}`));
119
+ return;
120
+ }
121
+ const output = Buffer.concat(stdout).toString("utf8").trim();
122
+ try {
123
+ const result = JSON.parse(output);
124
+ if (!isValidWaitResult(result))
125
+ scheduleProcessGroupTermination(child.pid);
126
+ resolve(result);
127
+ }
128
+ catch {
129
+ scheduleProcessGroupTermination(child.pid);
130
+ reject(new Error("continuation adapter must emit exactly one JSON result on stdout"));
131
+ }
132
+ });
133
+ child.stdin.on("error", (error) => {
134
+ if (error.code !== "EPIPE" && !settled) {
135
+ settled = true;
136
+ clearTimeout(timeout);
137
+ if (forcedKill)
138
+ clearTimeout(forcedKill);
139
+ scheduleProcessGroupTermination(child.pid);
140
+ reject(error);
141
+ }
142
+ });
143
+ child.stdin.end(`${JSON.stringify(request)}\n`);
144
+ });
145
+ }
146
+ function validateAdapterCommand(value) {
147
+ if (!value || typeof value !== "object" || Array.isArray(value))
148
+ throw new Error("continuation adapter command file must contain an object");
149
+ const argv = value.argv;
150
+ if (!Array.isArray(argv) || argv.length === 0 || argv.length > 128 || argv.some((entry) => typeof entry !== "string" || entry.length === 0 || entry.includes("\0"))) {
151
+ throw new Error("continuation adapter command argv must contain 1 through 128 non-empty strings");
152
+ }
153
+ return { argv: [...argv] };
154
+ }
155
+ function assertRegularFile(file, label) {
156
+ const stat = fs.lstatSync(file);
157
+ if (stat.isSymbolicLink() || !stat.isFile())
158
+ throw new Error(`${label} must be a regular file`);
159
+ }
160
+ function readRegularFileNoFollow(file, label) {
161
+ return readRegularFileBufferNoFollow(file, label).toString("utf8");
162
+ }
163
+ function readRegularFileBufferNoFollow(file, label) {
164
+ assertRegularFile(file, label);
165
+ const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
166
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
167
+ try {
168
+ if (!fs.fstatSync(fd).isFile())
169
+ throw new Error(`${label} must be a regular file`);
170
+ return fs.readFileSync(fd);
171
+ }
172
+ finally {
173
+ fs.closeSync(fd);
174
+ }
175
+ }
176
+ function assertPositiveInteger(value, label, min, max) {
177
+ if (!Number.isSafeInteger(value) || value < min || value > max)
178
+ throw new Error(`${label} must be an integer from ${min} through ${max}`);
179
+ }
180
+ function pidAlive(pid) {
181
+ try {
182
+ process.kill(pid, 0);
183
+ return true;
184
+ }
185
+ catch (error) {
186
+ return error.code === "EPERM";
187
+ }
188
+ }
189
+ function terminateProcessGroup(pid, signal) {
190
+ if (!pid)
191
+ return;
192
+ try {
193
+ process.kill(process.platform === "win32" ? pid : -pid, signal);
194
+ }
195
+ catch (error) {
196
+ if (error.code !== "ESRCH")
197
+ throw error;
198
+ }
199
+ }
200
+ function isValidWaitResult(result) {
201
+ if (result?.status !== "wait" || !result.barrier || typeof result.barrier !== "object")
202
+ return false;
203
+ if (result.barrier.kind === "pid")
204
+ return Number.isSafeInteger(result.barrier.pid) && result.barrier.pid > 0;
205
+ return result.barrier.kind === "deadline" && typeof result.barrier.at === "string" && Number.isFinite(Date.parse(result.barrier.at));
206
+ }
207
+ function scheduleProcessGroupTermination(pid) {
208
+ terminateProcessGroup(pid, "SIGTERM");
209
+ const forcedKill = setTimeout(() => terminateProcessGroup(pid, "SIGKILL"), 250);
210
+ forcedKill.unref();
211
+ }
212
+ function regularFileExists(file) {
213
+ try {
214
+ const stat = fs.lstatSync(file);
215
+ return !stat.isSymbolicLink() && stat.isFile();
216
+ }
217
+ catch (error) {
218
+ if (error.code === "ENOENT")
219
+ return false;
220
+ throw error;
221
+ }
222
+ }
223
+ function sha256File(file, label) {
224
+ return createHash("sha256").update(readRegularFileBufferNoFollow(file, label)).digest("hex");
225
+ }
@@ -6,6 +6,7 @@ import { isDeepStrictEqual } from "node:util";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { validateDefinition } from "@kontourai/flow";
8
8
  import { loadBuilderFlowRun } from "../builder-flow-run-adapter.js";
9
+ import { driveBuilderFlowSession, withContinuationDriverLock } from "../continuation-driver.js";
9
10
  import { inspectBuilderFlowSession, recoverBuilderFlowSession, syncBuilderFlowSession } from "../builder-flow-runtime.js";
10
11
  import { flowAgentsPackageRoot, flowAgentsPackageVersion } from "../lib/package-version.js";
11
12
  import { pinnedFlowAgentsCommand } from "../lib/pinned-cli-command.js";
@@ -14,12 +15,13 @@ import { flagBool, flagList, flagString, parseArgs } from "../lib/args.js";
14
15
  import { main as builderRun } from "./builder-run.js";
15
16
  import { currentWorkflowSessionDir, isMeaningfulTestCommand, mainFromPublicWorkflow, WORKFLOW_WRITER_CONTRACT_VERSION } from "./workflow-sidecar.js";
16
17
  import { resolveCurrentAssignmentActor, withSubjectLock } from "./assignment-provider.js";
18
+ import { executeLoadedContinuationAdapter, loadContinuationAdapterCommand, waitForContinuationBarrier } from "./continuation-adapter.js";
17
19
  export const WORKFLOW_CONTRACT_VERSION = "1.0";
18
20
  const PACKAGE_ROOT = flowAgentsPackageRoot();
19
21
  const REQUIRE = createRequire(import.meta.url);
20
22
  const PACKAGE_METADATA = readJsonFile(path.join(PACKAGE_ROOT, "package.json"), "Flow Agents package metadata");
21
23
  const CLI_VERSION = flowAgentsPackageVersion();
22
- const PUBLIC_VERBS = ["start", "status", "evidence", "critique", "pause", "resume", "release", "cancel", "archive", "doctor"];
24
+ const PUBLIC_VERBS = ["start", "status", "evidence", "critique", "drive", "pause", "resume", "release", "cancel", "archive", "doctor"];
23
25
  function usage() {
24
26
  console.log(`Usage: flow-agents workflow <verb> [options]
25
27
 
@@ -28,6 +30,7 @@ Public workflow verbs:
28
30
  status Show the current canonical run and projected next action.
29
31
  evidence Record evidence for the current Flow gate and synchronize it.
30
32
  critique Record review critique directly into the current trust bundle.
33
+ drive Continue the canonical run through an explicit runtime adapter.
31
34
  pause Pause the current run as its assignment actor.
32
35
  resume Resume the current paused run as its assignment actor.
33
36
  release Release the current assignment without canceling the run.
@@ -60,11 +63,43 @@ export async function main(argv) {
60
63
  return evidence(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
61
64
  if (verb === "critique")
62
65
  return critique(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
66
+ if (verb === "drive")
67
+ return drive(sessionDir, argv.slice(1), flagBool(parsed.flags, "json"));
63
68
  const forwarded = stripPublicFlags(argv.slice(1), new Set(["artifact-root", "session-dir", "json"]));
64
69
  if (verb === "release" && !flagString(parsed.flags, "reason"))
65
70
  throw new Error("workflow release requires --reason <text>");
66
71
  return builderRun([verb === "release" ? "release-assignment" : verb, "--session-dir", sessionDir, ...forwarded]);
67
72
  }
73
+ async function drive(sessionDir, argv, json) {
74
+ const parsed = parseArgs(argv);
75
+ assertOnlyFlags(parsed.flags, new Set(["artifact-root", "session-dir", "json", "adapter-command-file", "max-turns", "turn-timeout-ms", "barrier-wait-ms", "barrier-poll-ms"]), "workflow drive");
76
+ const adapterCommandFile = flagString(parsed.flags, "adapter-command-file");
77
+ if (!adapterCommandFile)
78
+ throw new Error("workflow drive requires --adapter-command-file <path>");
79
+ const maxTurns = integerFlag(parsed.flags, "max-turns", 4, 1, 100);
80
+ const turnTimeoutMs = integerFlag(parsed.flags, "turn-timeout-ms", 900_000, 1, 86_400_000);
81
+ const barrierWaitMs = integerFlag(parsed.flags, "barrier-wait-ms", 300_000, 0, 86_400_000);
82
+ const barrierPollMs = integerFlag(parsed.flags, "barrier-poll-ms", 1_000, 1, 60_000);
83
+ const { slug, projectRoot } = readBoundSession(sessionDir);
84
+ assertMatchingAssignmentActor(sessionDir, slug);
85
+ const adapterCommand = loadContinuationAdapterCommand(adapterCommandFile);
86
+ const result = await withContinuationDriverLock(sessionDir, async () => {
87
+ assertMatchingAssignmentActor(sessionDir, slug);
88
+ return driveBuilderFlowSession({
89
+ sessionDir,
90
+ maxTurns,
91
+ adapterCommandIdentity: adapterCommand.identity,
92
+ authorizeTurn: async () => { assertMatchingAssignmentActor(sessionDir, slug); },
93
+ execute: async (request) => executeLoadedContinuationAdapter(adapterCommand, request, { cwd: projectRoot, timeoutMs: turnTimeoutMs }),
94
+ waitForBarrier: async (barrier) => waitForContinuationBarrier(barrier, { maxWaitMs: barrierWaitMs, pollMs: barrierPollMs }),
95
+ });
96
+ });
97
+ if (json)
98
+ console.log(JSON.stringify(result));
99
+ else
100
+ console.log(`Continuation driver ${result.outcome} after ${result.turns_started} turn(s); canonical Flow is ${result.snapshot.status} at ${result.snapshot.current_step}.`);
101
+ return 0;
102
+ }
68
103
  async function start(argv) {
69
104
  const parsed = parseArgs(argv);
70
105
  assertOnlyFlags(parsed.flags, new Set(["flow", "work-item", "task-slug", "artifact-root", "source-request", "summary", "title", "criterion", "assignment-provider", "effective-state-json"]), "workflow start");
@@ -610,6 +645,17 @@ function assertOnlyFlags(flags, allowed, command) {
610
645
  if (unsupported)
611
646
  throw new Error(`${command} does not support --${unsupported}`);
612
647
  }
648
+ function integerFlag(flags, name, fallback, min, max) {
649
+ const raw = flagString(flags, name);
650
+ if (raw === undefined)
651
+ return fallback;
652
+ if (!/^(0|[1-9][0-9]*)$/.test(raw))
653
+ throw new Error(`workflow drive --${name} must be an integer from ${min} through ${max}`);
654
+ const value = Number(raw);
655
+ if (!Number.isSafeInteger(value) || value < min || value > max)
656
+ throw new Error(`workflow drive --${name} must be an integer from ${min} through ${max}`);
657
+ return value;
658
+ }
613
659
  function isWithin(candidate, root) {
614
660
  const relative = path.relative(root, candidate);
615
661
  return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
@@ -0,0 +1,92 @@
1
+ export type ContinuationBarrier = {
2
+ kind: "pid";
3
+ pid: number;
4
+ } | {
5
+ kind: "deadline";
6
+ at: string;
7
+ };
8
+ export type ContinuationSnapshot = {
9
+ run_id: string;
10
+ definition_id: string;
11
+ status: string;
12
+ disposition: "continue" | "waiting" | "done" | "failed";
13
+ current_step: string;
14
+ next_action: Record<string, unknown> | null;
15
+ };
16
+ export type ContinuationTurnRequest = {
17
+ schema_version: "1.0";
18
+ run_id: string;
19
+ definition_id: string;
20
+ current_step: string;
21
+ iteration: number;
22
+ max_turns: number;
23
+ next_action: Record<string, unknown> | null;
24
+ };
25
+ export type ContinuationTurnResult = {
26
+ status: "completed";
27
+ summary?: string;
28
+ } | {
29
+ status: "wait";
30
+ barrier: ContinuationBarrier;
31
+ summary?: string;
32
+ };
33
+ export type ContinuationDriverState = {
34
+ schema_version: "1.0";
35
+ run_id: string;
36
+ definition_id: string;
37
+ max_turns: number;
38
+ adapter_command_identity: string | null;
39
+ status: "active" | "waiting" | "done" | "failed" | "budget_exhausted";
40
+ turns_started: number;
41
+ pending_barrier: ContinuationBarrier | null;
42
+ updated_at: string;
43
+ };
44
+ export type ContinuationDriverEvent = {
45
+ schema_version: "1.0";
46
+ type: "started" | "turn_started" | "turn_completed" | "turn_failed" | "parked" | "resumed" | "done" | "budget_exhausted";
47
+ run_id: string;
48
+ definition_id: string;
49
+ current_step: string;
50
+ turns_started: number;
51
+ at: string;
52
+ barrier?: ContinuationBarrier;
53
+ summary?: string;
54
+ };
55
+ export interface ContinuationStateStore {
56
+ load(): ContinuationDriverState | null;
57
+ save(state: ContinuationDriverState): void;
58
+ append(event: ContinuationDriverEvent): void;
59
+ }
60
+ export interface ContinuationRuntimePort {
61
+ inspect(): Promise<ContinuationSnapshot>;
62
+ synchronize(): Promise<ContinuationSnapshot>;
63
+ execute(request: ContinuationTurnRequest): Promise<ContinuationTurnResult>;
64
+ }
65
+ export type ContinuationDriverOutcome = {
66
+ outcome: "done" | "waiting" | "failed" | "budget_exhausted";
67
+ turns_started: number;
68
+ snapshot: ContinuationSnapshot;
69
+ barrier?: ContinuationBarrier;
70
+ };
71
+ export interface RunContinuationDriverInput {
72
+ maxTurns: number;
73
+ adapterCommandIdentity?: string;
74
+ runtime: ContinuationRuntimePort;
75
+ store: ContinuationStateStore;
76
+ waitForBarrier?: (barrier: ContinuationBarrier) => Promise<"ready" | "pending">;
77
+ authorizeTurn?: () => Promise<void>;
78
+ now?: () => Date;
79
+ }
80
+ export interface DriveBuilderFlowSessionInput {
81
+ sessionDir: string;
82
+ maxTurns: number;
83
+ adapterCommandIdentity?: string;
84
+ execute: ContinuationRuntimePort["execute"];
85
+ waitForBarrier?: RunContinuationDriverInput["waitForBarrier"];
86
+ authorizeTurn?: RunContinuationDriverInput["authorizeTurn"];
87
+ now?: () => Date;
88
+ }
89
+ export declare function runContinuationDriver(input: RunContinuationDriverInput): Promise<ContinuationDriverOutcome>;
90
+ export declare function driveBuilderFlowSession(input: DriveBuilderFlowSessionInput): Promise<ContinuationDriverOutcome>;
91
+ export declare function withContinuationDriverLock<T>(sessionDirInput: string, body: () => Promise<T>): Promise<T>;
92
+ export declare function createFileContinuationStore(sessionDirInput: string): ContinuationStateStore;