@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.
@@ -0,0 +1,532 @@
1
+ import * as fs from "node:fs";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { randomUUID } from "node:crypto";
4
+ import * as path from "node:path";
5
+ import { inspectBuilderFlowSession, syncBuilderFlowSession } from "./builder-flow-runtime.js";
6
+ import { atomicWriteJson, ensureSafeDirectory } from "./lib/fs.js";
7
+
8
+ export type ContinuationBarrier =
9
+ | { kind: "pid"; pid: number }
10
+ | { kind: "deadline"; at: string };
11
+
12
+ export type ContinuationSnapshot = {
13
+ run_id: string;
14
+ definition_id: string;
15
+ status: string;
16
+ disposition: "continue" | "waiting" | "done" | "failed";
17
+ current_step: string;
18
+ next_action: Record<string, unknown> | null;
19
+ };
20
+
21
+ export type ContinuationTurnRequest = {
22
+ schema_version: "1.0";
23
+ run_id: string;
24
+ definition_id: string;
25
+ current_step: string;
26
+ iteration: number;
27
+ max_turns: number;
28
+ next_action: Record<string, unknown> | null;
29
+ };
30
+
31
+ export type ContinuationTurnResult =
32
+ | { status: "completed"; summary?: string }
33
+ | { status: "wait"; barrier: ContinuationBarrier; summary?: string };
34
+
35
+ export type ContinuationDriverState = {
36
+ schema_version: "1.0";
37
+ run_id: string;
38
+ definition_id: string;
39
+ max_turns: number;
40
+ adapter_command_identity: string | null;
41
+ status: "active" | "waiting" | "done" | "failed" | "budget_exhausted";
42
+ turns_started: number;
43
+ pending_barrier: ContinuationBarrier | null;
44
+ updated_at: string;
45
+ };
46
+
47
+ export type ContinuationDriverEvent = {
48
+ schema_version: "1.0";
49
+ type: "started" | "turn_started" | "turn_completed" | "turn_failed" | "parked" | "resumed" | "done" | "budget_exhausted";
50
+ run_id: string;
51
+ definition_id: string;
52
+ current_step: string;
53
+ turns_started: number;
54
+ at: string;
55
+ barrier?: ContinuationBarrier;
56
+ summary?: string;
57
+ };
58
+
59
+ export interface ContinuationStateStore {
60
+ load(): ContinuationDriverState | null;
61
+ save(state: ContinuationDriverState): void;
62
+ append(event: ContinuationDriverEvent): void;
63
+ }
64
+
65
+ export interface ContinuationRuntimePort {
66
+ inspect(): Promise<ContinuationSnapshot>;
67
+ synchronize(): Promise<ContinuationSnapshot>;
68
+ execute(request: ContinuationTurnRequest): Promise<ContinuationTurnResult>;
69
+ }
70
+
71
+ export type ContinuationDriverOutcome = {
72
+ outcome: "done" | "waiting" | "failed" | "budget_exhausted";
73
+ turns_started: number;
74
+ snapshot: ContinuationSnapshot;
75
+ barrier?: ContinuationBarrier;
76
+ };
77
+
78
+ export interface RunContinuationDriverInput {
79
+ maxTurns: number;
80
+ adapterCommandIdentity?: string;
81
+ runtime: ContinuationRuntimePort;
82
+ store: ContinuationStateStore;
83
+ waitForBarrier?: (barrier: ContinuationBarrier) => Promise<"ready" | "pending">;
84
+ authorizeTurn?: () => Promise<void>;
85
+ now?: () => Date;
86
+ }
87
+
88
+ export interface DriveBuilderFlowSessionInput {
89
+ sessionDir: string;
90
+ maxTurns: number;
91
+ adapterCommandIdentity?: string;
92
+ execute: ContinuationRuntimePort["execute"];
93
+ waitForBarrier?: RunContinuationDriverInput["waitForBarrier"];
94
+ authorizeTurn?: RunContinuationDriverInput["authorizeTurn"];
95
+ now?: () => Date;
96
+ }
97
+
98
+ export async function runContinuationDriver(input: RunContinuationDriverInput): Promise<ContinuationDriverOutcome> {
99
+ assertMaxTurns(input.maxTurns);
100
+ const now = input.now ?? (() => new Date());
101
+ const waitForBarrier = input.waitForBarrier ?? (async () => "pending" as const);
102
+ const authorizeTurn = input.authorizeTurn ?? (async () => {});
103
+ const adapterCommandIdentity = input.adapterCommandIdentity ?? null;
104
+ let snapshot = validateSnapshot(await input.runtime.inspect());
105
+ let state = loadOrCreateState(input.store, snapshot, input.maxTurns, adapterCommandIdentity, now);
106
+ if (state.max_turns !== input.maxTurns) throw new Error(`continuation maxTurns ${input.maxTurns} does not match the persisted mission budget ${state.max_turns}`);
107
+ if (state.adapter_command_identity !== adapterCommandIdentity) throw new Error("continuation adapter command identity does not match the persisted mission adapter");
108
+
109
+ const inspectedOutcome = canonicalOutcome(snapshot);
110
+ if (inspectedOutcome === "done") return finishDone(input.store, state, snapshot, now);
111
+ if (inspectedOutcome === "failed") return finishFailed(input.store, state, snapshot, now);
112
+
113
+ if (state.pending_barrier) {
114
+ const barrier = state.pending_barrier;
115
+ const readiness = await waitForBarrier(barrier);
116
+ if (readiness === "pending") {
117
+ state = saveState(input.store, state, { status: "waiting" }, now);
118
+ return { outcome: "waiting", turns_started: state.turns_started, snapshot, barrier };
119
+ }
120
+ state = saveState(input.store, state, { status: "active", pending_barrier: null }, now);
121
+ appendEvent(input.store, state, snapshot, "resumed", now, { barrier });
122
+ }
123
+
124
+ snapshot = validateSnapshot(await input.runtime.synchronize());
125
+ assertMissionIdentity(state, snapshot);
126
+ const initialOutcome = canonicalOutcome(snapshot);
127
+ if (initialOutcome === "done") return finishDone(input.store, state, snapshot, now);
128
+ if (initialOutcome === "failed") return finishFailed(input.store, state, snapshot, now);
129
+ if (initialOutcome === "waiting") {
130
+ state = saveState(input.store, state, { status: "waiting" }, now);
131
+ return { outcome: "waiting", turns_started: state.turns_started, snapshot };
132
+ }
133
+
134
+ while (state.turns_started < input.maxTurns) {
135
+ await authorizeTurn();
136
+ const iteration = state.turns_started + 1;
137
+ state = saveState(input.store, state, { status: "active", turns_started: iteration }, now);
138
+ appendEvent(input.store, state, snapshot, "turn_started", now);
139
+ let result: ContinuationTurnResult;
140
+ try {
141
+ result = validateTurnResult(await input.runtime.execute(Object.freeze({
142
+ schema_version: "1.0",
143
+ run_id: snapshot.run_id,
144
+ definition_id: snapshot.definition_id,
145
+ current_step: snapshot.current_step,
146
+ iteration,
147
+ max_turns: input.maxTurns,
148
+ next_action: snapshot.next_action ? structuredClone(snapshot.next_action) : null,
149
+ })));
150
+ } catch (error) {
151
+ appendEvent(input.store, state, snapshot, "turn_failed", now, { summary: boundedErrorMessage(error) });
152
+ snapshot = validateSnapshot(await input.runtime.synchronize());
153
+ assertMissionIdentity(state, snapshot);
154
+ const outcome = canonicalOutcome(snapshot);
155
+ if (outcome === "done") return finishDone(input.store, state, snapshot, now);
156
+ if (outcome === "failed") return finishFailed(input.store, state, snapshot, now);
157
+ if (outcome === "waiting") {
158
+ state = saveState(input.store, state, { status: "waiting" }, now);
159
+ return { outcome: "waiting", turns_started: state.turns_started, snapshot };
160
+ }
161
+ continue;
162
+ }
163
+ appendEvent(input.store, state, snapshot, "turn_completed", now, { summary: result.summary });
164
+
165
+ if (result.status === "wait") {
166
+ state = saveState(input.store, state, { status: "waiting", pending_barrier: result.barrier }, now);
167
+ appendEvent(input.store, state, snapshot, "parked", now, { barrier: result.barrier, summary: result.summary });
168
+ const readiness = await waitForBarrier(result.barrier);
169
+ if (readiness === "pending") {
170
+ return { outcome: "waiting", turns_started: state.turns_started, snapshot, barrier: result.barrier };
171
+ }
172
+ state = saveState(input.store, state, { status: "active", pending_barrier: null }, now);
173
+ appendEvent(input.store, state, snapshot, "resumed", now, { barrier: result.barrier });
174
+ }
175
+
176
+ snapshot = validateSnapshot(await input.runtime.synchronize());
177
+ assertMissionIdentity(state, snapshot);
178
+ const outcome = canonicalOutcome(snapshot);
179
+ if (outcome === "done") return finishDone(input.store, state, snapshot, now);
180
+ if (outcome === "failed") return finishFailed(input.store, state, snapshot, now);
181
+ if (outcome === "waiting") {
182
+ state = saveState(input.store, state, { status: "waiting" }, now);
183
+ return { outcome: "waiting", turns_started: state.turns_started, snapshot };
184
+ }
185
+ }
186
+
187
+ state = saveState(input.store, state, { status: "budget_exhausted" }, now);
188
+ appendEvent(input.store, state, snapshot, "budget_exhausted", now);
189
+ return { outcome: "budget_exhausted", turns_started: state.turns_started, snapshot };
190
+ }
191
+
192
+ export async function driveBuilderFlowSession(input: DriveBuilderFlowSessionInput): Promise<ContinuationDriverOutcome> {
193
+ const sessionDir = path.resolve(input.sessionDir);
194
+ const runtime: ContinuationRuntimePort = {
195
+ inspect: async () => builderSessionSnapshot(await inspectBuilderFlowSession({ sessionDir })),
196
+ synchronize: async () => builderSessionSnapshot(await syncBuilderFlowSession({ sessionDir })),
197
+ execute: input.execute,
198
+ };
199
+ return runContinuationDriver({
200
+ maxTurns: input.maxTurns,
201
+ ...(input.adapterCommandIdentity ? { adapterCommandIdentity: input.adapterCommandIdentity } : {}),
202
+ runtime,
203
+ store: createFileContinuationStore(sessionDir),
204
+ ...(input.waitForBarrier ? { waitForBarrier: input.waitForBarrier } : {}),
205
+ ...(input.authorizeTurn ? { authorizeTurn: input.authorizeTurn } : {}),
206
+ ...(input.now ? { now: input.now } : {}),
207
+ });
208
+ }
209
+
210
+ export async function withContinuationDriverLock<T>(sessionDirInput: string, body: () => Promise<T>): Promise<T> {
211
+ const sessionDir = path.resolve(sessionDirInput);
212
+ const driverDir = path.join(sessionDir, "continuation-driver");
213
+ const locksDir = path.join(driverDir, "locks");
214
+ ensureSafeDirectory(sessionDir, locksDir);
215
+ const token = randomUUID();
216
+ const lockFile = path.join(locksDir, `${process.pid}-${token}.lock`);
217
+ const owner = { schema_version: "1.0" as const, pid: process.pid, token, created_at: new Date().toISOString() };
218
+ acquireDriverLock(locksDir, lockFile, owner);
219
+ try {
220
+ return await body();
221
+ } finally {
222
+ releaseDriverLock(lockFile, token);
223
+ }
224
+ }
225
+
226
+ export function createFileContinuationStore(sessionDirInput: string): ContinuationStateStore {
227
+ const sessionDir = path.resolve(sessionDirInput);
228
+ const driverDir = path.join(sessionDir, "continuation-driver");
229
+ const stateFile = path.join(driverDir, "state.json");
230
+ const eventsFile = path.join(driverDir, "events.jsonl");
231
+ ensureSafeDirectory(sessionDir, driverDir);
232
+ return {
233
+ load(): ContinuationDriverState | null {
234
+ if (!fs.existsSync(stateFile)) {
235
+ if (fs.existsSync(eventsFile) && fs.statSync(eventsFile).size > 0) throw new Error("continuation driver state is missing while mission events exist");
236
+ return null;
237
+ }
238
+ const state = JSON.parse(readRegularFileNoFollow(stateFile, "continuation driver state")) as ContinuationDriverState;
239
+ reconcileStateWithEvents(state, eventsFile);
240
+ return state;
241
+ },
242
+ save(state: ContinuationDriverState): void {
243
+ atomicWriteJson(sessionDir, stateFile, state);
244
+ },
245
+ append(event: ContinuationDriverEvent): void {
246
+ appendJsonLineNoFollow(sessionDir, eventsFile, event);
247
+ },
248
+ };
249
+ }
250
+
251
+ function builderSessionSnapshot(result: Awaited<ReturnType<typeof inspectBuilderFlowSession>>): ContinuationSnapshot {
252
+ const nextAction = result.projection.next_action;
253
+ return validateSnapshot({
254
+ run_id: result.run.runId,
255
+ definition_id: result.run.definitionId,
256
+ status: result.run.state.status,
257
+ disposition: builderContinuationDisposition(result.projection.next_action),
258
+ current_step: result.run.state.current_step,
259
+ next_action: nextAction && typeof nextAction === "object" && !Array.isArray(nextAction)
260
+ ? structuredClone(nextAction as Record<string, unknown>)
261
+ : null,
262
+ });
263
+ }
264
+
265
+ function appendJsonLineNoFollow(root: string, file: string, value: unknown): void {
266
+ ensureSafeDirectory(root, path.dirname(file));
267
+ if (fs.existsSync(file)) assertRegularFile(file, "continuation driver event log");
268
+ const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
269
+ const fd = fs.openSync(file, fsConstants.O_WRONLY | fsConstants.O_APPEND | fsConstants.O_CREAT | noFollow, 0o600);
270
+ try {
271
+ fs.writeFileSync(fd, `${JSON.stringify(value)}\n`, "utf8");
272
+ } finally {
273
+ fs.closeSync(fd);
274
+ }
275
+ }
276
+
277
+ function assertRegularFile(file: string, label: string): void {
278
+ const stat = fs.lstatSync(file);
279
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`${label} must be a regular file`);
280
+ }
281
+
282
+ function readRegularFileNoFollow(file: string, label: string): string {
283
+ assertRegularFile(file, label);
284
+ const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
285
+ const fd = fs.openSync(file, fsConstants.O_RDONLY | noFollow);
286
+ try {
287
+ if (!fs.fstatSync(fd).isFile()) throw new Error(`${label} must be a regular file`);
288
+ return fs.readFileSync(fd, "utf8");
289
+ } finally {
290
+ fs.closeSync(fd);
291
+ }
292
+ }
293
+
294
+ function acquireDriverLock(
295
+ locksDir: string,
296
+ lockFile: string,
297
+ owner: { schema_version: "1.0"; pid: number; token: string; created_at: string },
298
+ ): void {
299
+ const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
300
+ const fd = fs.openSync(lockFile, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollow, 0o600);
301
+ try {
302
+ fs.writeFileSync(fd, `${JSON.stringify(owner)}\n`, "utf8");
303
+ } finally {
304
+ fs.closeSync(fd);
305
+ }
306
+ try {
307
+ for (const name of fs.readdirSync(locksDir).sort()) {
308
+ const candidate = path.join(locksDir, name);
309
+ if (candidate === lockFile) continue;
310
+ let existing: { schema_version?: unknown; pid?: unknown; token?: unknown };
311
+ try {
312
+ existing = JSON.parse(readRegularFileNoFollow(candidate, "continuation driver lock")) as typeof existing;
313
+ } catch (error) {
314
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
315
+ throw error;
316
+ }
317
+ if (existing.schema_version !== "1.0" || typeof existing.pid !== "number" || !Number.isSafeInteger(existing.pid) || existing.pid <= 0 || typeof existing.token !== "string") {
318
+ throw new Error("continuation driver lock is invalid");
319
+ }
320
+ if (processAlive(existing.pid)) throw new Error(`continuation driver is already running under pid ${existing.pid}`);
321
+ try {
322
+ fs.unlinkSync(candidate);
323
+ } catch (error) {
324
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
325
+ }
326
+ }
327
+ } catch (error) {
328
+ try {
329
+ fs.unlinkSync(lockFile);
330
+ } catch {}
331
+ throw error;
332
+ }
333
+ }
334
+
335
+ function releaseDriverLock(lockFile: string, token: string): void {
336
+ try {
337
+ const owner = JSON.parse(readRegularFileNoFollow(lockFile, "continuation driver lock")) as { token?: unknown };
338
+ if (owner.token === token) fs.unlinkSync(lockFile);
339
+ } catch (error) {
340
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
341
+ }
342
+ }
343
+
344
+ function loadOrCreateState(
345
+ store: ContinuationStateStore,
346
+ snapshot: ContinuationSnapshot,
347
+ maxTurns: number,
348
+ adapterCommandIdentity: string | null,
349
+ now: () => Date,
350
+ ): ContinuationDriverState {
351
+ const existing = store.load();
352
+ if (existing) {
353
+ validateState(existing);
354
+ assertMissionIdentity(existing, snapshot);
355
+ return existing;
356
+ }
357
+ const state: ContinuationDriverState = {
358
+ schema_version: "1.0",
359
+ run_id: snapshot.run_id,
360
+ definition_id: snapshot.definition_id,
361
+ max_turns: maxTurns,
362
+ adapter_command_identity: adapterCommandIdentity,
363
+ status: "active",
364
+ turns_started: 0,
365
+ pending_barrier: null,
366
+ updated_at: now().toISOString(),
367
+ };
368
+ store.save(state);
369
+ appendEvent(store, state, snapshot, "started", now);
370
+ return state;
371
+ }
372
+
373
+ function canonicalOutcome(snapshot: ContinuationSnapshot): "done" | "waiting" | "failed" | "continue" {
374
+ return snapshot.disposition;
375
+ }
376
+
377
+ function finishDone(store: ContinuationStateStore, state: ContinuationDriverState, snapshot: ContinuationSnapshot, now: () => Date): ContinuationDriverOutcome {
378
+ const done = saveState(store, state, { status: "done", pending_barrier: null }, now);
379
+ appendEvent(store, done, snapshot, "done", now);
380
+ return { outcome: "done", turns_started: done.turns_started, snapshot };
381
+ }
382
+
383
+ function finishFailed(store: ContinuationStateStore, state: ContinuationDriverState, snapshot: ContinuationSnapshot, now: () => Date): ContinuationDriverOutcome {
384
+ const failed = saveState(store, state, { status: "failed", pending_barrier: null }, now);
385
+ return { outcome: "failed", turns_started: failed.turns_started, snapshot };
386
+ }
387
+
388
+ function saveState(
389
+ store: ContinuationStateStore,
390
+ current: ContinuationDriverState,
391
+ patch: Partial<Pick<ContinuationDriverState, "status" | "turns_started" | "pending_barrier">>,
392
+ now: () => Date,
393
+ ): ContinuationDriverState {
394
+ const next = { ...current, ...patch, updated_at: now().toISOString() };
395
+ store.save(next);
396
+ return next;
397
+ }
398
+
399
+ function appendEvent(
400
+ store: ContinuationStateStore,
401
+ state: ContinuationDriverState,
402
+ snapshot: ContinuationSnapshot,
403
+ type: ContinuationDriverEvent["type"],
404
+ now: () => Date,
405
+ extra: Pick<ContinuationDriverEvent, "barrier" | "summary"> = {},
406
+ ): void {
407
+ store.append({
408
+ schema_version: "1.0",
409
+ type,
410
+ run_id: state.run_id,
411
+ definition_id: state.definition_id,
412
+ current_step: snapshot.current_step,
413
+ turns_started: state.turns_started,
414
+ at: now().toISOString(),
415
+ ...(extra.barrier ? { barrier: extra.barrier } : {}),
416
+ ...(extra.summary ? { summary: extra.summary } : {}),
417
+ });
418
+ }
419
+
420
+ function validateSnapshot(value: ContinuationSnapshot): ContinuationSnapshot {
421
+ if (!value || typeof value !== "object") throw new Error("continuation runtime returned an invalid canonical snapshot");
422
+ for (const field of ["run_id", "definition_id", "status", "current_step"] as const) {
423
+ if (typeof value[field] !== "string" || value[field].length === 0) throw new Error(`continuation snapshot ${field} must be a non-empty string`);
424
+ }
425
+ if (!new Set(["continue", "waiting", "done", "failed"]).has(value.disposition)) throw new Error("continuation snapshot disposition is invalid");
426
+ if (value.next_action !== null && (typeof value.next_action !== "object" || Array.isArray(value.next_action))) {
427
+ throw new Error("continuation snapshot next_action must be an object or null");
428
+ }
429
+ return structuredClone(value);
430
+ }
431
+
432
+ function validateTurnResult(value: ContinuationTurnResult): ContinuationTurnResult {
433
+ if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait")) {
434
+ throw new Error("continuation adapter must return status completed or wait");
435
+ }
436
+ if (value.status === "wait") validateBarrier(value.barrier);
437
+ if (value.summary !== undefined && typeof value.summary !== "string") throw new Error("continuation adapter summary must be a string");
438
+ const copy = structuredClone(value);
439
+ if (copy.summary && copy.summary.length > 2_000) copy.summary = `${copy.summary.slice(0, 1_997)}...`;
440
+ return copy;
441
+ }
442
+
443
+ function validateBarrier(barrier: ContinuationBarrier): void {
444
+ if (!barrier || typeof barrier !== "object") throw new Error("continuation wait result requires a barrier");
445
+ if (barrier.kind === "pid") {
446
+ if (!Number.isSafeInteger(barrier.pid) || barrier.pid <= 0) throw new Error("continuation pid barrier requires a positive integer pid");
447
+ return;
448
+ }
449
+ if (barrier.kind === "deadline") {
450
+ if (typeof barrier.at !== "string" || !Number.isFinite(Date.parse(barrier.at))) throw new Error("continuation deadline barrier requires an ISO date-time");
451
+ return;
452
+ }
453
+ throw new Error("continuation barrier kind must be pid or deadline");
454
+ }
455
+
456
+ function validateState(state: ContinuationDriverState): void {
457
+ if (state.schema_version !== "1.0") throw new Error("continuation driver state schema_version must be 1.0");
458
+ assertMaxTurns(state.max_turns);
459
+ if (state.adapter_command_identity !== null && (typeof state.adapter_command_identity !== "string" || state.adapter_command_identity.length === 0)) {
460
+ throw new Error("continuation driver adapter_command_identity must be a non-empty string or null");
461
+ }
462
+ if (!Number.isSafeInteger(state.turns_started) || state.turns_started < 0) throw new Error("continuation driver turns_started must be a non-negative integer");
463
+ if (state.turns_started > state.max_turns) throw new Error("continuation driver turns_started cannot exceed max_turns");
464
+ if (!new Set(["active", "waiting", "done", "failed", "budget_exhausted"]).has(state.status)) throw new Error("continuation driver state status is invalid");
465
+ if (state.status === "budget_exhausted" && state.turns_started !== state.max_turns) {
466
+ throw new Error("continuation driver budget_exhausted state must consume max_turns");
467
+ }
468
+ if (state.pending_barrier) validateBarrier(state.pending_barrier);
469
+ }
470
+
471
+ function assertMissionIdentity(state: Pick<ContinuationDriverState, "run_id" | "definition_id">, snapshot: ContinuationSnapshot): void {
472
+ if (state.run_id !== snapshot.run_id || state.definition_id !== snapshot.definition_id) {
473
+ throw new Error("continuation driver mission identity does not match the canonical Flow run");
474
+ }
475
+ }
476
+
477
+ function assertMaxTurns(value: number): void {
478
+ if (!Number.isSafeInteger(value) || value < 1 || value > 100) throw new Error("continuation maxTurns must be an integer from 1 through 100");
479
+ }
480
+
481
+ function boundedErrorMessage(error: unknown): string {
482
+ const message = error instanceof Error ? error.message : String(error);
483
+ return message.length <= 2_000 ? message : `${message.slice(0, 1_997)}...`;
484
+ }
485
+
486
+ function processAlive(pid: number): boolean {
487
+ try {
488
+ process.kill(pid, 0);
489
+ return true;
490
+ } catch (error) {
491
+ return (error as NodeJS.ErrnoException).code === "EPERM";
492
+ }
493
+ }
494
+
495
+ function reconcileStateWithEvents(state: ContinuationDriverState, eventsFile: string): void {
496
+ if (!fs.existsSync(eventsFile)) {
497
+ if (state.turns_started > 0) throw new Error("continuation driver event history is missing for a started mission");
498
+ return;
499
+ }
500
+ const lines = readRegularFileNoFollow(eventsFile, "continuation driver event log").split("\n").filter((line) => line.length > 0);
501
+ let highestStartedTurn = 0;
502
+ let eventBarrier: ContinuationBarrier | null = null;
503
+ for (const line of lines) {
504
+ const event = JSON.parse(line) as Partial<ContinuationDriverEvent>;
505
+ if (event.run_id !== state.run_id || event.definition_id !== state.definition_id) throw new Error("continuation driver event history has a foreign mission identity");
506
+ if (event.type === "turn_started") {
507
+ if (!Number.isSafeInteger(event.turns_started) || (event.turns_started as number) < 1) throw new Error("continuation driver event history has an invalid turn count");
508
+ highestStartedTurn = Math.max(highestStartedTurn, event.turns_started as number);
509
+ }
510
+ if (event.type === "parked") {
511
+ validateBarrier(event.barrier as ContinuationBarrier);
512
+ eventBarrier = structuredClone(event.barrier as ContinuationBarrier);
513
+ }
514
+ if (event.type === "resumed") eventBarrier = null;
515
+ }
516
+ if (highestStartedTurn > state.turns_started) throw new Error("continuation driver state rolled back behind its event history");
517
+ if (state.turns_started > highestStartedTurn + 1) throw new Error("continuation driver state is ahead of its event history");
518
+ if (!state.pending_barrier && eventBarrier) {
519
+ state.pending_barrier = eventBarrier;
520
+ state.status = "waiting";
521
+ }
522
+ }
523
+
524
+ function builderContinuationDisposition(nextAction: unknown): ContinuationSnapshot["disposition"] {
525
+ if (!nextAction || typeof nextAction !== "object" || Array.isArray(nextAction)) throw new Error("Builder Flow projection is missing its canonical next action");
526
+ const status = (nextAction as { status?: unknown }).status;
527
+ if (status === "continue") return "continue";
528
+ if (status === "blocked") return "waiting";
529
+ if (status === "done") return "done";
530
+ if (status === "failed") return "failed";
531
+ throw new Error(`Builder Flow projection has unsupported next-action status: ${String(status)}`);
532
+ }
package/src/index.ts CHANGED
@@ -45,6 +45,26 @@ export {
45
45
  } from "./builder-flow-runtime.js";
46
46
  export type { BuilderFlowAgentLifecycleInput, BuilderFlowAuthorizedLifecycleInput, BuilderFlowSessionInput, BuilderFlowSessionResult } from "./builder-flow-runtime.js";
47
47
 
48
+ export {
49
+ createFileContinuationStore,
50
+ driveBuilderFlowSession,
51
+ runContinuationDriver,
52
+ withContinuationDriverLock,
53
+ } from "./continuation-driver.js";
54
+ export type {
55
+ ContinuationBarrier,
56
+ ContinuationDriverEvent,
57
+ ContinuationDriverOutcome,
58
+ ContinuationDriverState,
59
+ ContinuationRuntimePort,
60
+ ContinuationSnapshot,
61
+ ContinuationStateStore,
62
+ ContinuationTurnRequest,
63
+ ContinuationTurnResult,
64
+ DriveBuilderFlowSessionInput,
65
+ RunContinuationDriverInput,
66
+ } from "./continuation-driver.js";
67
+
48
68
  // Pure serialization contract used by external lifecycle authorities when
49
69
  // signing requests. This does not load, create, or mutate a Flow run.
50
70
  export { builderLifecycleAuthorizationPayload, loadBuilderLifecycleAuthorization } from "./builder-lifecycle-authority.js";