@kontourai/flow-agents 3.11.0 → 3.12.1
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 +14 -0
- package/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/build/src/builder-flow-run-adapter.js +2 -0
- package/build/src/builder-flow-runtime.d.ts +5 -0
- package/build/src/builder-flow-runtime.js +90 -68
- package/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/build/src/builder-gate-action-envelope.js +368 -0
- package/build/src/cli/public-contracts.d.ts +139 -0
- package/build/src/cli/public-contracts.js +37 -0
- package/build/src/cli/workflow-sidecar.js +9 -13
- package/build/src/cli/workflow.d.ts +1 -0
- package/build/src/cli/workflow.js +49 -12
- package/build/src/continuation-driver.d.ts +40 -3
- package/build/src/continuation-driver.js +361 -412
- package/build/src/continuation-persistence.d.ts +4 -0
- package/build/src/continuation-persistence.js +296 -0
- package/build/src/continuation-validation.d.ts +9 -0
- package/build/src/continuation-validation.js +169 -0
- package/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/build/src/flow-kit/action-metadata.js +162 -0
- package/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/build/src/flow-kit/action-repository-validation.js +237 -0
- package/build/src/flow-kit/validate.d.ts +2 -14
- package/build/src/flow-kit/validate.js +18 -345
- package/build/src/index.d.ts +4 -2
- package/build/src/index.js +2 -1
- package/dist/base/build/package.json +1 -1
- package/dist/base/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/dist/base/build/src/builder-flow-run-adapter.js +2 -0
- package/dist/base/build/src/builder-flow-runtime.d.ts +5 -0
- package/dist/base/build/src/builder-flow-runtime.js +90 -68
- package/dist/base/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/dist/base/build/src/builder-gate-action-envelope.js +368 -0
- package/dist/base/build/src/cli/public-contracts.d.ts +139 -0
- package/dist/base/build/src/cli/public-contracts.js +37 -0
- package/dist/base/build/src/cli/workflow-sidecar.js +9 -13
- package/dist/base/build/src/cli/workflow.d.ts +1 -0
- package/dist/base/build/src/cli/workflow.js +49 -12
- package/dist/base/build/src/continuation-driver.d.ts +40 -3
- package/dist/base/build/src/continuation-driver.js +361 -412
- package/dist/base/build/src/continuation-persistence.d.ts +4 -0
- package/dist/base/build/src/continuation-persistence.js +296 -0
- package/dist/base/build/src/continuation-validation.d.ts +9 -0
- package/dist/base/build/src/continuation-validation.js +169 -0
- package/dist/base/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/dist/base/build/src/flow-kit/action-metadata.js +162 -0
- package/dist/base/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/dist/base/build/src/flow-kit/action-repository-validation.js +237 -0
- package/dist/base/build/src/flow-kit/validate.d.ts +2 -14
- package/dist/base/build/src/flow-kit/validate.js +18 -345
- package/dist/base/build/src/index.d.ts +4 -2
- package/dist/base/build/src/index.js +2 -1
- package/dist/base/docs/migrations.md +5 -0
- package/dist/base/docs/public-workflow-cli.md +41 -5
- package/dist/base/docs/spec/builder-flow-runtime.md +117 -0
- package/dist/base/evals/integration/test_builder_step_producers.sh +60 -21
- package/dist/base/evals/integration/test_bundle_install.sh +15 -15
- package/dist/base/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/dist/base/install.sh +1 -1
- package/dist/base/kits/builder/kit.json +88 -14
- package/dist/claude-code/build/package.json +1 -1
- package/dist/claude-code/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/dist/claude-code/build/src/builder-flow-run-adapter.js +2 -0
- package/dist/claude-code/build/src/builder-flow-runtime.d.ts +5 -0
- package/dist/claude-code/build/src/builder-flow-runtime.js +90 -68
- package/dist/claude-code/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/dist/claude-code/build/src/builder-gate-action-envelope.js +368 -0
- package/dist/claude-code/build/src/cli/public-contracts.d.ts +139 -0
- package/dist/claude-code/build/src/cli/public-contracts.js +37 -0
- package/dist/claude-code/build/src/cli/workflow-sidecar.js +9 -13
- package/dist/claude-code/build/src/cli/workflow.d.ts +1 -0
- package/dist/claude-code/build/src/cli/workflow.js +49 -12
- package/dist/claude-code/build/src/continuation-driver.d.ts +40 -3
- package/dist/claude-code/build/src/continuation-driver.js +361 -412
- package/dist/claude-code/build/src/continuation-persistence.d.ts +4 -0
- package/dist/claude-code/build/src/continuation-persistence.js +296 -0
- package/dist/claude-code/build/src/continuation-validation.d.ts +9 -0
- package/dist/claude-code/build/src/continuation-validation.js +169 -0
- package/dist/claude-code/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/dist/claude-code/build/src/flow-kit/action-metadata.js +162 -0
- package/dist/claude-code/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/dist/claude-code/build/src/flow-kit/action-repository-validation.js +237 -0
- package/dist/claude-code/build/src/flow-kit/validate.d.ts +2 -14
- package/dist/claude-code/build/src/flow-kit/validate.js +18 -345
- package/dist/claude-code/build/src/index.d.ts +4 -2
- package/dist/claude-code/build/src/index.js +2 -1
- package/dist/claude-code/docs/migrations.md +5 -0
- package/dist/claude-code/docs/public-workflow-cli.md +41 -5
- package/dist/claude-code/docs/spec/builder-flow-runtime.md +117 -0
- package/dist/claude-code/evals/integration/test_builder_step_producers.sh +60 -21
- package/dist/claude-code/evals/integration/test_bundle_install.sh +15 -15
- package/dist/claude-code/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/dist/claude-code/install.sh +1 -1
- package/dist/claude-code/kits/builder/kit.json +88 -14
- package/dist/codex/build/package.json +1 -1
- package/dist/codex/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/dist/codex/build/src/builder-flow-run-adapter.js +2 -0
- package/dist/codex/build/src/builder-flow-runtime.d.ts +5 -0
- package/dist/codex/build/src/builder-flow-runtime.js +90 -68
- package/dist/codex/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/dist/codex/build/src/builder-gate-action-envelope.js +368 -0
- package/dist/codex/build/src/cli/public-contracts.d.ts +139 -0
- package/dist/codex/build/src/cli/public-contracts.js +37 -0
- package/dist/codex/build/src/cli/workflow-sidecar.js +9 -13
- package/dist/codex/build/src/cli/workflow.d.ts +1 -0
- package/dist/codex/build/src/cli/workflow.js +49 -12
- package/dist/codex/build/src/continuation-driver.d.ts +40 -3
- package/dist/codex/build/src/continuation-driver.js +361 -412
- package/dist/codex/build/src/continuation-persistence.d.ts +4 -0
- package/dist/codex/build/src/continuation-persistence.js +296 -0
- package/dist/codex/build/src/continuation-validation.d.ts +9 -0
- package/dist/codex/build/src/continuation-validation.js +169 -0
- package/dist/codex/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/dist/codex/build/src/flow-kit/action-metadata.js +162 -0
- package/dist/codex/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/dist/codex/build/src/flow-kit/action-repository-validation.js +237 -0
- package/dist/codex/build/src/flow-kit/validate.d.ts +2 -14
- package/dist/codex/build/src/flow-kit/validate.js +18 -345
- package/dist/codex/build/src/index.d.ts +4 -2
- package/dist/codex/build/src/index.js +2 -1
- package/dist/codex/docs/migrations.md +5 -0
- package/dist/codex/docs/public-workflow-cli.md +41 -5
- package/dist/codex/docs/spec/builder-flow-runtime.md +117 -0
- package/dist/codex/evals/integration/test_builder_step_producers.sh +60 -21
- package/dist/codex/evals/integration/test_bundle_install.sh +15 -15
- package/dist/codex/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/dist/codex/install.sh +1 -1
- package/dist/codex/kits/builder/kit.json +88 -14
- package/dist/kiro/build/package.json +1 -1
- package/dist/kiro/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/dist/kiro/build/src/builder-flow-run-adapter.js +2 -0
- package/dist/kiro/build/src/builder-flow-runtime.d.ts +5 -0
- package/dist/kiro/build/src/builder-flow-runtime.js +90 -68
- package/dist/kiro/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/dist/kiro/build/src/builder-gate-action-envelope.js +368 -0
- package/dist/kiro/build/src/cli/public-contracts.d.ts +139 -0
- package/dist/kiro/build/src/cli/public-contracts.js +37 -0
- package/dist/kiro/build/src/cli/workflow-sidecar.js +9 -13
- package/dist/kiro/build/src/cli/workflow.d.ts +1 -0
- package/dist/kiro/build/src/cli/workflow.js +49 -12
- package/dist/kiro/build/src/continuation-driver.d.ts +40 -3
- package/dist/kiro/build/src/continuation-driver.js +361 -412
- package/dist/kiro/build/src/continuation-persistence.d.ts +4 -0
- package/dist/kiro/build/src/continuation-persistence.js +296 -0
- package/dist/kiro/build/src/continuation-validation.d.ts +9 -0
- package/dist/kiro/build/src/continuation-validation.js +169 -0
- package/dist/kiro/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/dist/kiro/build/src/flow-kit/action-metadata.js +162 -0
- package/dist/kiro/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/dist/kiro/build/src/flow-kit/action-repository-validation.js +237 -0
- package/dist/kiro/build/src/flow-kit/validate.d.ts +2 -14
- package/dist/kiro/build/src/flow-kit/validate.js +18 -345
- package/dist/kiro/build/src/index.d.ts +4 -2
- package/dist/kiro/build/src/index.js +2 -1
- package/dist/kiro/docs/migrations.md +5 -0
- package/dist/kiro/docs/public-workflow-cli.md +41 -5
- package/dist/kiro/docs/spec/builder-flow-runtime.md +117 -0
- package/dist/kiro/evals/integration/test_builder_step_producers.sh +60 -21
- package/dist/kiro/evals/integration/test_bundle_install.sh +15 -15
- package/dist/kiro/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/dist/kiro/install.sh +1 -1
- package/dist/kiro/kits/builder/kit.json +88 -14
- package/dist/opencode/build/package.json +1 -1
- package/dist/opencode/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/dist/opencode/build/src/builder-flow-run-adapter.js +2 -0
- package/dist/opencode/build/src/builder-flow-runtime.d.ts +5 -0
- package/dist/opencode/build/src/builder-flow-runtime.js +90 -68
- package/dist/opencode/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/dist/opencode/build/src/builder-gate-action-envelope.js +368 -0
- package/dist/opencode/build/src/cli/public-contracts.d.ts +139 -0
- package/dist/opencode/build/src/cli/public-contracts.js +37 -0
- package/dist/opencode/build/src/cli/workflow-sidecar.js +9 -13
- package/dist/opencode/build/src/cli/workflow.d.ts +1 -0
- package/dist/opencode/build/src/cli/workflow.js +49 -12
- package/dist/opencode/build/src/continuation-driver.d.ts +40 -3
- package/dist/opencode/build/src/continuation-driver.js +361 -412
- package/dist/opencode/build/src/continuation-persistence.d.ts +4 -0
- package/dist/opencode/build/src/continuation-persistence.js +296 -0
- package/dist/opencode/build/src/continuation-validation.d.ts +9 -0
- package/dist/opencode/build/src/continuation-validation.js +169 -0
- package/dist/opencode/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/dist/opencode/build/src/flow-kit/action-metadata.js +162 -0
- package/dist/opencode/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/dist/opencode/build/src/flow-kit/action-repository-validation.js +237 -0
- package/dist/opencode/build/src/flow-kit/validate.d.ts +2 -14
- package/dist/opencode/build/src/flow-kit/validate.js +18 -345
- package/dist/opencode/build/src/index.d.ts +4 -2
- package/dist/opencode/build/src/index.js +2 -1
- package/dist/opencode/docs/migrations.md +5 -0
- package/dist/opencode/docs/public-workflow-cli.md +41 -5
- package/dist/opencode/docs/spec/builder-flow-runtime.md +117 -0
- package/dist/opencode/evals/integration/test_builder_step_producers.sh +60 -21
- package/dist/opencode/evals/integration/test_bundle_install.sh +15 -15
- package/dist/opencode/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/dist/opencode/install.sh +1 -1
- package/dist/opencode/kits/builder/kit.json +88 -14
- package/dist/pi/build/package.json +1 -1
- package/dist/pi/build/src/builder-flow-run-adapter.d.ts +1 -0
- package/dist/pi/build/src/builder-flow-run-adapter.js +2 -0
- package/dist/pi/build/src/builder-flow-runtime.d.ts +5 -0
- package/dist/pi/build/src/builder-flow-runtime.js +90 -68
- package/dist/pi/build/src/builder-gate-action-envelope.d.ts +152 -0
- package/dist/pi/build/src/builder-gate-action-envelope.js +368 -0
- package/dist/pi/build/src/cli/public-contracts.d.ts +139 -0
- package/dist/pi/build/src/cli/public-contracts.js +37 -0
- package/dist/pi/build/src/cli/workflow-sidecar.js +9 -13
- package/dist/pi/build/src/cli/workflow.d.ts +1 -0
- package/dist/pi/build/src/cli/workflow.js +49 -12
- package/dist/pi/build/src/continuation-driver.d.ts +40 -3
- package/dist/pi/build/src/continuation-driver.js +361 -412
- package/dist/pi/build/src/continuation-persistence.d.ts +4 -0
- package/dist/pi/build/src/continuation-persistence.js +296 -0
- package/dist/pi/build/src/continuation-validation.d.ts +9 -0
- package/dist/pi/build/src/continuation-validation.js +169 -0
- package/dist/pi/build/src/flow-kit/action-metadata.d.ts +28 -0
- package/dist/pi/build/src/flow-kit/action-metadata.js +162 -0
- package/dist/pi/build/src/flow-kit/action-repository-validation.d.ts +16 -0
- package/dist/pi/build/src/flow-kit/action-repository-validation.js +237 -0
- package/dist/pi/build/src/flow-kit/validate.d.ts +2 -14
- package/dist/pi/build/src/flow-kit/validate.js +18 -345
- package/dist/pi/build/src/index.d.ts +4 -2
- package/dist/pi/build/src/index.js +2 -1
- package/dist/pi/docs/migrations.md +5 -0
- package/dist/pi/docs/public-workflow-cli.md +41 -5
- package/dist/pi/docs/spec/builder-flow-runtime.md +117 -0
- package/dist/pi/evals/integration/test_builder_step_producers.sh +60 -21
- package/dist/pi/evals/integration/test_bundle_install.sh +15 -15
- package/dist/pi/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/dist/pi/install.sh +1 -1
- package/dist/pi/kits/builder/kit.json +88 -14
- package/docs/migrations.md +5 -0
- package/docs/public-workflow-cli.md +41 -5
- package/docs/spec/builder-flow-runtime.md +117 -0
- package/evals/integration/test_builder_step_producers.sh +60 -21
- package/evals/integration/test_bundle_install.sh +15 -15
- package/evals/integration/test_phase_map_and_gate_claim.sh +9 -22
- package/kits/builder/kit.json +88 -14
- package/package.json +1 -1
- package/src/builder-flow-run-adapter.ts +3 -0
- package/src/builder-flow-runtime.ts +73 -46
- package/src/builder-gate-action-envelope.ts +531 -0
- package/src/cli/builder-flow-runtime.test.mjs +289 -58
- package/src/cli/continuation-driver.test.mjs +542 -24
- package/src/cli/kit-metadata-security.test.mjs +161 -8
- package/src/cli/public-contracts.ts +41 -0
- package/src/cli/workflow-sidecar.ts +9 -13
- package/src/cli/workflow.ts +48 -12
- package/src/continuation-driver.ts +466 -378
- package/src/continuation-persistence.ts +228 -0
- package/src/continuation-validation.ts +137 -0
- package/src/flow-kit/action-metadata.ts +155 -0
- package/src/flow-kit/action-repository-validation.ts +192 -0
- package/src/flow-kit/validate.ts +19 -326
- package/src/index.ts +17 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import { constants as fsConstants } from "node:fs";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
1
|
import * as path from "node:path";
|
|
5
2
|
import { inspectBuilderFlowSession, syncBuilderFlowSession } from "./builder-flow-runtime.js";
|
|
6
|
-
import {
|
|
3
|
+
import { gateActionProgressSnapshot, withGateActionPriorProgress, } from "./builder-gate-action-envelope.js";
|
|
4
|
+
import { createFileContinuationStore } from "./continuation-persistence.js";
|
|
5
|
+
import { assertMaxTurns, assertMissionIdentity, validateSnapshot, validateState, validateTurnResult } from "./continuation-validation.js";
|
|
6
|
+
export { MAX_CONTINUATION_TURNS, createFileContinuationStore, withContinuationDriverLock } from "./continuation-persistence.js";
|
|
7
|
+
export { MAX_CONTINUATION_ADAPTER_EVIDENCE_BYTES, MAX_CONTINUATION_TURN_RESULT_BYTES } from "./continuation-validation.js";
|
|
7
8
|
export class ContinuationAdapterTimeoutError extends Error {
|
|
8
9
|
code = "CONTINUATION_ADAPTER_TIMEOUT";
|
|
9
10
|
constructor(timeoutMs) {
|
|
@@ -14,155 +15,242 @@ export class ContinuationAdapterTimeoutError extends Error {
|
|
|
14
15
|
export async function runContinuationDriver(input) {
|
|
15
16
|
assertMaxTurns(input.maxTurns);
|
|
16
17
|
const now = input.now ?? (() => new Date());
|
|
17
|
-
const waitForBarrier = input.waitForBarrier ?? (async () => "pending");
|
|
18
|
-
const authorizeTurn = input.authorizeTurn ?? (async () => { });
|
|
19
|
-
const issueTurnAuthority = input.issueTurnAuthority;
|
|
20
18
|
const adapterCommandIdentity = input.adapterCommandIdentity ?? null;
|
|
21
|
-
|
|
22
|
-
let state = loadOrCreateState(input.store,
|
|
19
|
+
const inspected = validateSnapshot(await input.runtime.inspect());
|
|
20
|
+
let state = loadOrCreateState(input.store, inspected, input.maxTurns, adapterCommandIdentity, now);
|
|
23
21
|
if (state.max_turns !== input.maxTurns)
|
|
24
22
|
throw new Error(`continuation maxTurns ${input.maxTurns} does not match the persisted mission budget ${state.max_turns}`);
|
|
25
23
|
if (state.adapter_command_identity !== adapterCommandIdentity)
|
|
26
24
|
throw new Error("continuation adapter command identity does not match the persisted mission adapter");
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
25
|
+
let settled = await settleMissionStart(input, state, now);
|
|
26
|
+
if (settled.outcome)
|
|
27
|
+
return settled.outcome;
|
|
28
|
+
({ state } = settled);
|
|
29
|
+
let snapshot = settled.snapshot;
|
|
30
|
+
while (state.turns_started < input.maxTurns) {
|
|
31
|
+
const turn = await executeContinuationTurn(input, state, snapshot, now);
|
|
32
|
+
if (turn.outcome)
|
|
33
|
+
return turn.outcome;
|
|
34
|
+
state = turn.state;
|
|
35
|
+
snapshot = turn.snapshot;
|
|
36
|
+
}
|
|
37
|
+
return finishBudgetExhausted(input.store, state, snapshot, now);
|
|
38
|
+
}
|
|
39
|
+
async function settleMissionStart(input, state, now) {
|
|
40
|
+
let position = await synchronizeAndRecover(input, state, now);
|
|
41
|
+
if (position.state.pending_barrier && new Set(["done", "failed"]).has(canonicalOutcome(position.snapshot))) {
|
|
42
|
+
const barrier = position.state.pending_barrier;
|
|
43
|
+
position.state = saveState(input.store, position.state, { status: "active", pending_barrier: null }, now);
|
|
44
|
+
appendEvent(input.store, position.state, position.snapshot, "resumed", now, { barrier, summary: "canonical Flow resolved the pending barrier" });
|
|
45
|
+
}
|
|
46
|
+
const terminal = resolveCanonicalDisposition(input.store, position.state, position.snapshot, now, false);
|
|
47
|
+
if (terminal)
|
|
48
|
+
return { ...position, outcome: terminal };
|
|
49
|
+
if (position.state.pending_barrier) {
|
|
50
|
+
const barrier = position.state.pending_barrier;
|
|
51
|
+
const readiness = await (input.waitForBarrier ?? (async () => "pending"))(barrier);
|
|
35
52
|
if (readiness === "pending") {
|
|
36
|
-
|
|
37
|
-
return { outcome: "waiting", turns_started:
|
|
53
|
+
const waiting = saveState(input.store, position.state, { status: "waiting" }, now);
|
|
54
|
+
return { state: waiting, snapshot: position.snapshot, outcome: { outcome: "waiting", turns_started: waiting.turns_started, snapshot: position.snapshot, barrier } };
|
|
38
55
|
}
|
|
39
|
-
state = saveState(input.store, state, { status: "active",
|
|
40
|
-
appendEvent(input.store, state, snapshot, "resumed", now, { barrier });
|
|
56
|
+
position.state = saveState(input.store, position.state, { status: "active", pending_barrier: null }, now);
|
|
57
|
+
appendEvent(input.store, position.state, position.snapshot, "resumed", now, { barrier });
|
|
58
|
+
position = await synchronizeAndRecover(input, position.state, now);
|
|
41
59
|
}
|
|
42
|
-
|
|
60
|
+
const disposition = resolveCanonicalDisposition(input.store, position.state, position.snapshot, now, true);
|
|
61
|
+
return disposition ? { ...position, outcome: disposition } : position;
|
|
62
|
+
}
|
|
63
|
+
async function synchronizeAndRecover(input, state, now) {
|
|
64
|
+
const snapshot = validateSnapshot(await input.runtime.synchronize());
|
|
43
65
|
assertMissionIdentity(state, snapshot);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
const recovered = reconcileInterruptedTurn(input.store, state, snapshot, now);
|
|
67
|
+
return { state: reconcileProgress(input.store, recovered, snapshot, now), snapshot };
|
|
68
|
+
}
|
|
69
|
+
function resolveCanonicalDisposition(store, state, snapshot, now, includeWaiting) {
|
|
70
|
+
const disposition = canonicalOutcome(snapshot);
|
|
71
|
+
if (disposition === "done")
|
|
72
|
+
return finishDone(store, state, snapshot, now);
|
|
73
|
+
if (disposition === "failed")
|
|
74
|
+
return finishFailed(store, state, snapshot, now);
|
|
75
|
+
if (includeWaiting && disposition === "waiting") {
|
|
76
|
+
const waiting = saveState(store, state, { status: "waiting" }, now);
|
|
77
|
+
return { outcome: "waiting", turns_started: waiting.turns_started, snapshot };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
async function executeContinuationTurn(input, current, snapshot, now) {
|
|
82
|
+
await (input.authorizeTurn ?? (async () => { }))();
|
|
83
|
+
const request = continuationTurnRequest(input, current, snapshot);
|
|
84
|
+
if (input.preflightTurn)
|
|
85
|
+
await input.preflightTurn(request);
|
|
86
|
+
let state = beginContinuationTurn(input.store, current, snapshot, request.iteration, now);
|
|
87
|
+
let authority;
|
|
88
|
+
try {
|
|
89
|
+
authority = input.issueTurnAuthority ? await input.issueTurnAuthority(request) : undefined;
|
|
90
|
+
if (authority)
|
|
91
|
+
state = saveState(input.store, state, { active_turn_public_key_digest: authority.publicKeyDigest }, now);
|
|
92
|
+
const result = validateTurnResult(await input.runtime.execute(request, authorityContext(authority)));
|
|
93
|
+
return await recordAcceptedTurn(input, state, snapshot, request, result, now);
|
|
52
94
|
}
|
|
53
|
-
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
turns_started: iteration,
|
|
59
|
-
active_turn_step: snapshot.current_step,
|
|
60
|
-
active_turn_public_key_digest: null,
|
|
61
|
-
}, now);
|
|
62
|
-
appendEvent(input.store, state, snapshot, "turn_started", now);
|
|
63
|
-
let result;
|
|
64
|
-
let request;
|
|
65
|
-
let authority;
|
|
66
|
-
let terminalized = false;
|
|
67
|
-
try {
|
|
68
|
-
request = Object.freeze({
|
|
69
|
-
schema_version: "1.0",
|
|
70
|
-
run_id: snapshot.run_id,
|
|
71
|
-
definition_id: snapshot.definition_id,
|
|
72
|
-
current_step: snapshot.current_step,
|
|
73
|
-
iteration,
|
|
74
|
-
max_turns: input.maxTurns,
|
|
75
|
-
next_action: snapshot.next_action ? structuredClone(snapshot.next_action) : null,
|
|
76
|
-
});
|
|
77
|
-
authority = issueTurnAuthority ? await issueTurnAuthority(request) : undefined;
|
|
78
|
-
if (authority) {
|
|
79
|
-
state = saveState(input.store, state, { active_turn_public_key_digest: authority.publicKeyDigest }, now);
|
|
80
|
-
}
|
|
81
|
-
result = validateTurnResult(await input.runtime.execute(request, authority ? {
|
|
82
|
-
continuationTurnSecret: authority.turnSecret,
|
|
83
|
-
continuationRunId: authority.runId,
|
|
84
|
-
} : undefined));
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
const summary = boundedErrorMessage(error);
|
|
88
|
-
appendEvent(input.store, state, snapshot, "turn_failed", now, { summary, failure_kind: error instanceof ContinuationAdapterTimeoutError ? "timeout" : "adapter_error" });
|
|
89
|
-
snapshot = validateSnapshot(await input.runtime.synchronize());
|
|
90
|
-
assertMissionIdentity(state, snapshot);
|
|
91
|
-
const outcome = canonicalOutcome(snapshot);
|
|
92
|
-
if (outcome === "done") {
|
|
93
|
-
terminalized = true;
|
|
94
|
-
return finishDone(input.store, state, snapshot, now);
|
|
95
|
-
}
|
|
96
|
-
if (outcome === "failed") {
|
|
97
|
-
terminalized = true;
|
|
98
|
-
return finishFailed(input.store, state, snapshot, now);
|
|
99
|
-
}
|
|
100
|
-
if (outcome === "waiting") {
|
|
101
|
-
state = saveState(input.store, state, { status: "waiting" }, now);
|
|
102
|
-
return { outcome: "waiting", turns_started: state.turns_started, snapshot };
|
|
103
|
-
}
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
finally {
|
|
107
|
-
let cleanupFailure;
|
|
108
|
-
try {
|
|
109
|
-
if (authority && !authority.cleanup())
|
|
110
|
-
cleanupFailure = "continuation turn authority cleanup returned false";
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
cleanupFailure = boundedErrorMessage(error);
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
if (!terminalized)
|
|
117
|
-
state = saveState(input.store, state, {
|
|
118
|
-
active_turn_step: null,
|
|
119
|
-
active_turn_public_key_digest: null,
|
|
120
|
-
}, now);
|
|
121
|
-
if (cleanupFailure) {
|
|
122
|
-
try {
|
|
123
|
-
appendEvent(input.store, state, snapshot, "authority_cleanup_failed", now, { summary: cleanupFailure });
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
// Audit is best effort. A cleanup extension must not replace the adapter outcome.
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (input.onTurnAccepted)
|
|
132
|
-
await input.onTurnAccepted(request, result);
|
|
133
|
-
appendEvent(input.store, state, snapshot, "turn_completed", now, { summary: result.summary });
|
|
134
|
-
if (result.status === "wait") {
|
|
135
|
-
state = saveState(input.store, state, { status: "waiting", pending_barrier: result.barrier }, now);
|
|
136
|
-
appendEvent(input.store, state, snapshot, "parked", now, { barrier: result.barrier, summary: result.summary });
|
|
137
|
-
const readiness = await waitForBarrier(result.barrier);
|
|
138
|
-
if (readiness === "pending") {
|
|
139
|
-
return { outcome: "waiting", turns_started: state.turns_started, snapshot, barrier: result.barrier };
|
|
140
|
-
}
|
|
141
|
-
state = saveState(input.store, state, { status: "active", pending_barrier: null }, now);
|
|
142
|
-
appendEvent(input.store, state, snapshot, "resumed", now, { barrier: result.barrier });
|
|
143
|
-
}
|
|
144
|
-
snapshot = validateSnapshot(await input.runtime.synchronize());
|
|
145
|
-
assertMissionIdentity(state, snapshot);
|
|
146
|
-
if (snapshot.status === "active" && snapshot.current_step === request.current_step) {
|
|
147
|
-
appendEvent(input.store, state, snapshot, "gate_not_advanced", now, { summary: "adapter completed without canonical gate advancement" });
|
|
148
|
-
}
|
|
149
|
-
const outcome = canonicalOutcome(snapshot);
|
|
150
|
-
if (outcome === "done")
|
|
151
|
-
return finishDone(input.store, state, snapshot, now);
|
|
152
|
-
if (outcome === "failed")
|
|
153
|
-
return finishFailed(input.store, state, snapshot, now);
|
|
154
|
-
if (outcome === "waiting") {
|
|
155
|
-
state = saveState(input.store, state, { status: "waiting" }, now);
|
|
156
|
-
return { outcome: "waiting", turns_started: state.turns_started, snapshot };
|
|
157
|
-
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const failed = await recordFailedTurn(input, input.store.load() ?? state, snapshot, error, now);
|
|
97
|
+
if (isTurnCaptureFailure(error))
|
|
98
|
+
throw error.cause;
|
|
99
|
+
return failed;
|
|
158
100
|
}
|
|
159
|
-
|
|
160
|
-
|
|
101
|
+
finally {
|
|
102
|
+
auditAuthorityCleanup(input.store, state, snapshot, authority, now);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function continuationTurnRequest(input, state, snapshot) {
|
|
106
|
+
const envelope = snapshot.gate_action_envelope
|
|
107
|
+
? withGateActionPriorProgress(snapshot.gate_action_envelope, state.prior_progress ?? initialProgress())
|
|
108
|
+
: undefined;
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
schema_version: "1.0",
|
|
111
|
+
run_id: snapshot.run_id,
|
|
112
|
+
definition_id: snapshot.definition_id,
|
|
113
|
+
current_step: snapshot.current_step,
|
|
114
|
+
iteration: state.turns_started + 1,
|
|
115
|
+
max_turns: input.maxTurns,
|
|
116
|
+
next_action: snapshot.next_action ? structuredClone(snapshot.next_action) : null,
|
|
117
|
+
...(envelope ? { gate_action_envelope: envelope } : {}),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function beginContinuationTurn(store, state, snapshot, iteration, now) {
|
|
121
|
+
let started = saveState(store, state, {
|
|
122
|
+
status: "active", turns_started: iteration, active_turn_step: snapshot.current_step,
|
|
123
|
+
active_turn_public_key_digest: null, active_turn_phase: "prepared", active_turn_progress: progressSnapshot(snapshot), active_turn_capture: null,
|
|
124
|
+
}, now);
|
|
125
|
+
appendEvent(store, started, snapshot, "turn_started", now);
|
|
126
|
+
started = saveState(store, started, { active_turn_phase: "started" }, now);
|
|
127
|
+
return started;
|
|
128
|
+
}
|
|
129
|
+
function authorityContext(authority) {
|
|
130
|
+
return authority ? { continuationTurnSecret: authority.turnSecret, continuationRunId: authority.runId } : undefined;
|
|
131
|
+
}
|
|
132
|
+
async function captureAcceptedTurn(callback, request, result) {
|
|
133
|
+
try {
|
|
134
|
+
await callback(request, result);
|
|
135
|
+
}
|
|
136
|
+
catch (cause) {
|
|
137
|
+
const error = new Error(boundedErrorMessage(cause));
|
|
138
|
+
error.cause = cause;
|
|
139
|
+
error.capture_failure = true;
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function isTurnCaptureFailure(error) {
|
|
144
|
+
return error instanceof Error && error.capture_failure === true;
|
|
145
|
+
}
|
|
146
|
+
async function recordFailedTurn(input, state, previous, error, now) {
|
|
147
|
+
const measured = state.active_turn_phase === "measured" && state.active_turn_capture
|
|
148
|
+
? { state, snapshot: validateSnapshot(await input.runtime.synchronize()), progress: state.active_turn_capture.progress ?? undefined }
|
|
149
|
+
: await synchronizeTurnMeasurement(input, state, previous, null, null, now);
|
|
150
|
+
appendEvent(input.store, measured.state, measured.snapshot, "turn_failed", now, {
|
|
151
|
+
summary: boundedErrorMessage(error),
|
|
152
|
+
failure_kind: error instanceof ContinuationAdapterTimeoutError ? "timeout" : "adapter_error",
|
|
153
|
+
...(measured.progress ? { progress: measured.progress } : {}),
|
|
154
|
+
...(measured.state.active_turn_capture ? { turn_id: measured.state.active_turn_capture.turn_id } : {}),
|
|
155
|
+
});
|
|
156
|
+
const cleared = clearActiveTurn(input.store, measured.state, now);
|
|
157
|
+
const outcome = resolveCanonicalDisposition(input.store, cleared, measured.snapshot, now, true);
|
|
158
|
+
return outcome ? { state: cleared, snapshot: measured.snapshot, outcome } : { ...measured, state: cleared };
|
|
159
|
+
}
|
|
160
|
+
async function recordAcceptedTurn(input, state, previous, request, result, now) {
|
|
161
|
+
let measured = await synchronizeTurnMeasurement(input, state, previous, request, result, now);
|
|
162
|
+
const capture = measured.state.active_turn_capture;
|
|
163
|
+
acceptedTurnJournal(input.store)?.captureAcceptedTurn(capture);
|
|
164
|
+
if (input.onTurnAccepted)
|
|
165
|
+
await captureAcceptedTurn(input.onTurnAccepted, request, result);
|
|
166
|
+
if (result.status === "wait")
|
|
167
|
+
return parkAcceptedTurn(input, measured, result, now);
|
|
168
|
+
appendEvent(input.store, measured.state, measured.snapshot, "turn_completed", now, {
|
|
169
|
+
summary: result.summary,
|
|
170
|
+
...(measured.progress ? { progress: measured.progress } : {}),
|
|
171
|
+
turn_id: capture.turn_id,
|
|
172
|
+
});
|
|
173
|
+
measured = { ...measured, state: clearActiveTurn(input.store, measured.state, now) };
|
|
174
|
+
if (measured.snapshot.status === "active" && measured.snapshot.current_step === request.current_step) {
|
|
175
|
+
appendEvent(input.store, measured.state, measured.snapshot, "gate_not_advanced", now, { summary: "adapter completed without canonical gate advancement" });
|
|
176
|
+
}
|
|
177
|
+
const outcome = resolveCanonicalDisposition(input.store, measured.state, measured.snapshot, now, true);
|
|
178
|
+
return outcome ? { state: measured.state, snapshot: measured.snapshot, outcome } : measured;
|
|
179
|
+
}
|
|
180
|
+
async function parkAcceptedTurn(input, measured, result, now) {
|
|
181
|
+
let state = saveState(input.store, measured.state, { status: "waiting", pending_barrier: result.barrier }, now);
|
|
182
|
+
const turnId = state.active_turn_capture.turn_id;
|
|
183
|
+
appendEvent(input.store, state, measured.snapshot, "parked", now, {
|
|
184
|
+
barrier: result.barrier, summary: result.summary, ...(measured.progress ? { progress: measured.progress } : {}), turn_id: turnId,
|
|
185
|
+
});
|
|
186
|
+
state = clearActiveTurn(input.store, state, now);
|
|
187
|
+
const readiness = await (input.waitForBarrier ?? (async () => "pending"))(result.barrier);
|
|
188
|
+
if (readiness === "pending") {
|
|
189
|
+
return { state, snapshot: measured.snapshot, outcome: { outcome: "waiting", turns_started: state.turns_started, snapshot: measured.snapshot, barrier: result.barrier } };
|
|
190
|
+
}
|
|
191
|
+
state = saveState(input.store, state, { status: "active", pending_barrier: null }, now);
|
|
192
|
+
appendEvent(input.store, state, measured.snapshot, "resumed", now, { barrier: result.barrier });
|
|
193
|
+
const resumed = await synchronizeAndRecover(input, state, now);
|
|
194
|
+
const outcome = resolveCanonicalDisposition(input.store, resumed.state, resumed.snapshot, now, true);
|
|
195
|
+
return outcome ? { ...resumed, outcome } : resumed;
|
|
196
|
+
}
|
|
197
|
+
async function synchronizeTurnMeasurement(input, state, previous, request, result, now) {
|
|
198
|
+
const snapshot = validateSnapshot(await input.runtime.synchronize());
|
|
199
|
+
assertMissionIdentity(state, snapshot);
|
|
200
|
+
const before = state.active_turn_progress ?? state.last_progress ?? progressSnapshot(previous);
|
|
201
|
+
const progress = measureProgress(state, before, progressSnapshot(snapshot));
|
|
202
|
+
const capture = request && result ? acceptedTurnCapture(request, result, progress?.delta ?? null, now) : null;
|
|
203
|
+
const measured = saveState(input.store, state, {
|
|
204
|
+
...(progress ? { last_progress: progress.snapshot, prior_progress: progress.delta } : {}),
|
|
205
|
+
...(capture ? { active_turn_phase: "measured", active_turn_capture: capture } : {}),
|
|
206
|
+
}, now);
|
|
207
|
+
return { state: measured, snapshot, ...(progress ? { progress: progress.delta } : {}) };
|
|
208
|
+
}
|
|
209
|
+
function acceptedTurnCapture(request, result, progress, now) {
|
|
210
|
+
return {
|
|
211
|
+
schema_version: "1.0",
|
|
212
|
+
turn_id: `${request.run_id}:${request.iteration}`,
|
|
213
|
+
iteration: request.iteration,
|
|
214
|
+
request: structuredClone(request),
|
|
215
|
+
result: structuredClone(result),
|
|
216
|
+
progress: progress ? structuredClone(progress) : null,
|
|
217
|
+
captured_at: now().toISOString(),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function clearActiveTurn(store, state, now) {
|
|
221
|
+
return saveState(store, state, {
|
|
161
222
|
active_turn_step: null,
|
|
162
223
|
active_turn_public_key_digest: null,
|
|
224
|
+
active_turn_phase: null,
|
|
225
|
+
active_turn_progress: null,
|
|
226
|
+
active_turn_capture: null,
|
|
227
|
+
}, now);
|
|
228
|
+
}
|
|
229
|
+
function auditAuthorityCleanup(store, state, snapshot, authority, now) {
|
|
230
|
+
let summary;
|
|
231
|
+
try {
|
|
232
|
+
if (authority && !authority.cleanup())
|
|
233
|
+
summary = "continuation turn authority cleanup returned false";
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
summary = boundedErrorMessage(error);
|
|
237
|
+
}
|
|
238
|
+
if (!summary)
|
|
239
|
+
return;
|
|
240
|
+
try {
|
|
241
|
+
appendEvent(store, state, snapshot, "authority_cleanup_failed", now, { summary });
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Audit is best effort. A cleanup extension must not replace the adapter outcome.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function finishBudgetExhausted(store, state, snapshot, now) {
|
|
248
|
+
const exhausted = saveState(store, state, {
|
|
249
|
+
status: "budget_exhausted", active_turn_step: null, active_turn_public_key_digest: null,
|
|
250
|
+
active_turn_phase: null, active_turn_progress: null, active_turn_capture: null,
|
|
163
251
|
}, now);
|
|
164
|
-
appendEvent(
|
|
165
|
-
return { outcome: "budget_exhausted", turns_started:
|
|
252
|
+
appendEvent(store, exhausted, snapshot, "budget_exhausted", now);
|
|
253
|
+
return { outcome: "budget_exhausted", turns_started: exhausted.turns_started, snapshot };
|
|
166
254
|
}
|
|
167
255
|
export async function driveBuilderFlowSession(input) {
|
|
168
256
|
const sessionDir = path.resolve(input.sessionDir);
|
|
@@ -175,56 +263,15 @@ export async function driveBuilderFlowSession(input) {
|
|
|
175
263
|
maxTurns: input.maxTurns,
|
|
176
264
|
...(input.adapterCommandIdentity ? { adapterCommandIdentity: input.adapterCommandIdentity } : {}),
|
|
177
265
|
runtime,
|
|
178
|
-
store: createFileContinuationStore(sessionDir),
|
|
266
|
+
store: input.store ?? createFileContinuationStore(sessionDir),
|
|
179
267
|
...(input.waitForBarrier ? { waitForBarrier: input.waitForBarrier } : {}),
|
|
180
268
|
...(input.authorizeTurn ? { authorizeTurn: input.authorizeTurn } : {}),
|
|
181
269
|
...(input.issueTurnAuthority ? { issueTurnAuthority: input.issueTurnAuthority } : {}),
|
|
270
|
+
...(input.preflightTurn ? { preflightTurn: input.preflightTurn } : {}),
|
|
182
271
|
...(input.onTurnAccepted ? { onTurnAccepted: input.onTurnAccepted } : {}),
|
|
183
272
|
...(input.now ? { now: input.now } : {}),
|
|
184
273
|
});
|
|
185
274
|
}
|
|
186
|
-
export async function withContinuationDriverLock(sessionDirInput, body) {
|
|
187
|
-
const sessionDir = path.resolve(sessionDirInput);
|
|
188
|
-
const driverDir = path.join(sessionDir, "continuation-driver");
|
|
189
|
-
const locksDir = path.join(driverDir, "locks");
|
|
190
|
-
ensureSafeDirectory(sessionDir, locksDir);
|
|
191
|
-
const token = randomUUID();
|
|
192
|
-
const lockFile = path.join(locksDir, `${process.pid}-${token}.lock`);
|
|
193
|
-
const lease = { pid: process.pid, token, created_at: new Date().toISOString() };
|
|
194
|
-
const owner = { schema_version: "1.0", ...lease };
|
|
195
|
-
acquireDriverLock(locksDir, lockFile, owner);
|
|
196
|
-
try {
|
|
197
|
-
return await body(lease);
|
|
198
|
-
}
|
|
199
|
-
finally {
|
|
200
|
-
releaseDriverLock(lockFile, token);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
export function createFileContinuationStore(sessionDirInput) {
|
|
204
|
-
const sessionDir = path.resolve(sessionDirInput);
|
|
205
|
-
const driverDir = path.join(sessionDir, "continuation-driver");
|
|
206
|
-
const stateFile = path.join(driverDir, "state.json");
|
|
207
|
-
const eventsFile = path.join(driverDir, "events.jsonl");
|
|
208
|
-
ensureSafeDirectory(sessionDir, driverDir);
|
|
209
|
-
return {
|
|
210
|
-
load() {
|
|
211
|
-
if (!fs.existsSync(stateFile)) {
|
|
212
|
-
if (fs.existsSync(eventsFile) && fs.statSync(eventsFile).size > 0)
|
|
213
|
-
throw new Error("continuation driver state is missing while mission events exist");
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
const state = JSON.parse(readRegularFileNoFollow(stateFile, "continuation driver state"));
|
|
217
|
-
reconcileStateWithEvents(state, eventsFile);
|
|
218
|
-
return state;
|
|
219
|
-
},
|
|
220
|
-
save(state) {
|
|
221
|
-
atomicWriteJson(sessionDir, stateFile, state);
|
|
222
|
-
},
|
|
223
|
-
append(event) {
|
|
224
|
-
appendJsonLineNoFollow(sessionDir, eventsFile, event);
|
|
225
|
-
},
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
275
|
function builderSessionSnapshot(result) {
|
|
229
276
|
const nextAction = result.projection.next_action;
|
|
230
277
|
return validateSnapshot({
|
|
@@ -236,95 +283,10 @@ function builderSessionSnapshot(result) {
|
|
|
236
283
|
next_action: nextAction && typeof nextAction === "object" && !Array.isArray(nextAction)
|
|
237
284
|
? structuredClone(nextAction)
|
|
238
285
|
: null,
|
|
286
|
+
progress_snapshot: structuredClone(result.progressSnapshot),
|
|
287
|
+
...(result.gateActionEnvelope ? { gate_action_envelope: structuredClone(result.gateActionEnvelope) } : {}),
|
|
239
288
|
});
|
|
240
289
|
}
|
|
241
|
-
function appendJsonLineNoFollow(root, file, value) {
|
|
242
|
-
ensureSafeDirectory(root, path.dirname(file));
|
|
243
|
-
if (fs.existsSync(file))
|
|
244
|
-
assertRegularFile(file, "continuation driver event log");
|
|
245
|
-
const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
246
|
-
const fd = fs.openSync(file, fsConstants.O_WRONLY | fsConstants.O_APPEND | fsConstants.O_CREAT | noFollow, 0o600);
|
|
247
|
-
try {
|
|
248
|
-
fs.writeFileSync(fd, `${JSON.stringify(value)}\n`, "utf8");
|
|
249
|
-
}
|
|
250
|
-
finally {
|
|
251
|
-
fs.closeSync(fd);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
function assertRegularFile(file, label) {
|
|
255
|
-
const stat = fs.lstatSync(file);
|
|
256
|
-
if (stat.isSymbolicLink() || !stat.isFile())
|
|
257
|
-
throw new Error(`${label} must be a regular file`);
|
|
258
|
-
}
|
|
259
|
-
function readRegularFileNoFollow(file, label) {
|
|
260
|
-
assertRegularFile(file, label);
|
|
261
|
-
const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
262
|
-
const fd = fs.openSync(file, fsConstants.O_RDONLY | noFollow);
|
|
263
|
-
try {
|
|
264
|
-
if (!fs.fstatSync(fd).isFile())
|
|
265
|
-
throw new Error(`${label} must be a regular file`);
|
|
266
|
-
return fs.readFileSync(fd, "utf8");
|
|
267
|
-
}
|
|
268
|
-
finally {
|
|
269
|
-
fs.closeSync(fd);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
function acquireDriverLock(locksDir, lockFile, owner) {
|
|
273
|
-
const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
274
|
-
const fd = fs.openSync(lockFile, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollow, 0o600);
|
|
275
|
-
try {
|
|
276
|
-
fs.writeFileSync(fd, `${JSON.stringify(owner)}\n`, "utf8");
|
|
277
|
-
}
|
|
278
|
-
finally {
|
|
279
|
-
fs.closeSync(fd);
|
|
280
|
-
}
|
|
281
|
-
try {
|
|
282
|
-
for (const name of fs.readdirSync(locksDir).sort()) {
|
|
283
|
-
const candidate = path.join(locksDir, name);
|
|
284
|
-
if (candidate === lockFile)
|
|
285
|
-
continue;
|
|
286
|
-
let existing;
|
|
287
|
-
try {
|
|
288
|
-
existing = JSON.parse(readRegularFileNoFollow(candidate, "continuation driver lock"));
|
|
289
|
-
}
|
|
290
|
-
catch (error) {
|
|
291
|
-
if (error.code === "ENOENT")
|
|
292
|
-
continue;
|
|
293
|
-
throw error;
|
|
294
|
-
}
|
|
295
|
-
if (existing.schema_version !== "1.0" || typeof existing.pid !== "number" || !Number.isSafeInteger(existing.pid) || existing.pid <= 0 || typeof existing.token !== "string") {
|
|
296
|
-
throw new Error("continuation driver lock is invalid");
|
|
297
|
-
}
|
|
298
|
-
if (processAlive(existing.pid))
|
|
299
|
-
throw new Error(`continuation driver is already running under pid ${existing.pid}`);
|
|
300
|
-
try {
|
|
301
|
-
fs.unlinkSync(candidate);
|
|
302
|
-
}
|
|
303
|
-
catch (error) {
|
|
304
|
-
if (error.code !== "ENOENT")
|
|
305
|
-
throw error;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
try {
|
|
311
|
-
fs.unlinkSync(lockFile);
|
|
312
|
-
}
|
|
313
|
-
catch { }
|
|
314
|
-
throw error;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
function releaseDriverLock(lockFile, token) {
|
|
318
|
-
try {
|
|
319
|
-
const owner = JSON.parse(readRegularFileNoFollow(lockFile, "continuation driver lock"));
|
|
320
|
-
if (owner.token === token)
|
|
321
|
-
fs.unlinkSync(lockFile);
|
|
322
|
-
}
|
|
323
|
-
catch (error) {
|
|
324
|
-
if (error.code !== "ENOENT")
|
|
325
|
-
throw error;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
290
|
function loadOrCreateState(store, snapshot, maxTurns, adapterCommandIdentity, now) {
|
|
329
291
|
const existing = store.load();
|
|
330
292
|
if (existing) {
|
|
@@ -342,6 +304,9 @@ function loadOrCreateState(store, snapshot, maxTurns, adapterCommandIdentity, no
|
|
|
342
304
|
turns_started: 0,
|
|
343
305
|
active_turn_step: null,
|
|
344
306
|
active_turn_public_key_digest: null,
|
|
307
|
+
active_turn_phase: null,
|
|
308
|
+
active_turn_progress: null,
|
|
309
|
+
active_turn_capture: null,
|
|
345
310
|
pending_barrier: null,
|
|
346
311
|
updated_at: now().toISOString(),
|
|
347
312
|
};
|
|
@@ -352,11 +317,133 @@ function loadOrCreateState(store, snapshot, maxTurns, adapterCommandIdentity, no
|
|
|
352
317
|
function canonicalOutcome(snapshot) {
|
|
353
318
|
return snapshot.disposition;
|
|
354
319
|
}
|
|
320
|
+
function reconcileProgress(store, state, snapshot, now) {
|
|
321
|
+
const current = progressSnapshot(snapshot);
|
|
322
|
+
if (!current)
|
|
323
|
+
return state;
|
|
324
|
+
if (!state.last_progress)
|
|
325
|
+
return saveState(store, state, { last_progress: current, prior_progress: initialProgress() }, now);
|
|
326
|
+
if (sameProgressSnapshot(state.last_progress, current))
|
|
327
|
+
return state;
|
|
328
|
+
const progress = measureProgress(state, state.last_progress, current);
|
|
329
|
+
return progress ? saveState(store, state, { last_progress: progress.snapshot, prior_progress: progress.delta }, now) : state;
|
|
330
|
+
}
|
|
331
|
+
function reconcileInterruptedTurn(store, state, snapshot, now) {
|
|
332
|
+
if (!state.active_turn_phase)
|
|
333
|
+
return state;
|
|
334
|
+
if (state.active_turn_phase === "prepared") {
|
|
335
|
+
return clearActiveTurn(store, state, now);
|
|
336
|
+
}
|
|
337
|
+
if (state.active_turn_phase === "measured") {
|
|
338
|
+
if (!state.active_turn_capture)
|
|
339
|
+
throw new Error("continuation measured turn is missing its durable capture");
|
|
340
|
+
const capture = state.active_turn_capture;
|
|
341
|
+
acceptedTurnJournal(store)?.captureAcceptedTurn(capture);
|
|
342
|
+
let recovered = capture.result.status === "wait"
|
|
343
|
+
? saveState(store, state, { status: "waiting", pending_barrier: capture.result.barrier }, now)
|
|
344
|
+
: state;
|
|
345
|
+
appendEvent(store, recovered, snapshot, capture.result.status === "wait" ? "parked" : "turn_completed", now, {
|
|
346
|
+
...(capture.result.status === "wait" ? { barrier: capture.result.barrier } : {}),
|
|
347
|
+
...(capture.result.summary ? { summary: capture.result.summary } : {}),
|
|
348
|
+
...(capture.progress ? { progress: capture.progress } : {}),
|
|
349
|
+
turn_id: capture.turn_id,
|
|
350
|
+
});
|
|
351
|
+
recovered = clearActiveTurn(store, recovered, now);
|
|
352
|
+
appendEvent(store, recovered, snapshot, "turn_recovered", now, {
|
|
353
|
+
summary: "completed durable accepted-turn capture after interruption",
|
|
354
|
+
...(capture.progress ? { progress: capture.progress } : {}),
|
|
355
|
+
turn_id: capture.turn_id,
|
|
356
|
+
});
|
|
357
|
+
return recovered;
|
|
358
|
+
}
|
|
359
|
+
const progress = measureProgress(state, state.active_turn_progress ?? state.last_progress ?? null, progressSnapshot(snapshot));
|
|
360
|
+
const recovered = saveState(store, state, {
|
|
361
|
+
...(progress ? { last_progress: progress.snapshot, prior_progress: progress.delta } : {}),
|
|
362
|
+
active_turn_step: null,
|
|
363
|
+
active_turn_public_key_digest: null,
|
|
364
|
+
active_turn_phase: null,
|
|
365
|
+
active_turn_progress: null,
|
|
366
|
+
active_turn_capture: null,
|
|
367
|
+
}, now);
|
|
368
|
+
appendEvent(store, recovered, snapshot, "turn_recovered", now, {
|
|
369
|
+
summary: "recovered interrupted adapter turn from canonical Flow progress",
|
|
370
|
+
...(progress ? { progress: progress.delta } : {}),
|
|
371
|
+
});
|
|
372
|
+
return recovered;
|
|
373
|
+
}
|
|
374
|
+
function sameProgressSnapshot(left, right) {
|
|
375
|
+
return left.current_step === right.current_step
|
|
376
|
+
&& sameCanonicalStatus(left.canonical_status, right.canonical_status)
|
|
377
|
+
&& left.canonical_evidence.length === right.canonical_evidence.length
|
|
378
|
+
&& left.canonical_evidence.every((entry, index) => entry === right.canonical_evidence[index])
|
|
379
|
+
&& left.observed_artifacts.length === right.observed_artifacts.length
|
|
380
|
+
&& left.observed_artifacts.every((entry, index) => entry === right.observed_artifacts[index]);
|
|
381
|
+
}
|
|
382
|
+
function isTerminalCanonicalStatus(status) {
|
|
383
|
+
return status === "completed" || status === "failed";
|
|
384
|
+
}
|
|
385
|
+
function sameCanonicalStatus(left, right) {
|
|
386
|
+
if (left === right)
|
|
387
|
+
return true;
|
|
388
|
+
// Pre-status snapshots remain compatible while active. A newly observed
|
|
389
|
+
// terminal status is still a real canonical transition from legacy state.
|
|
390
|
+
if (left === undefined)
|
|
391
|
+
return !isTerminalCanonicalStatus(right);
|
|
392
|
+
if (right === undefined)
|
|
393
|
+
return !isTerminalCanonicalStatus(left);
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
function progressSnapshot(snapshot) {
|
|
397
|
+
if (snapshot.progress_snapshot)
|
|
398
|
+
return structuredClone(snapshot.progress_snapshot);
|
|
399
|
+
return snapshot.gate_action_envelope ? gateActionProgressSnapshot(snapshot.gate_action_envelope) : null;
|
|
400
|
+
}
|
|
401
|
+
function acceptedTurnJournal(store) {
|
|
402
|
+
const candidate = store;
|
|
403
|
+
return typeof candidate.captureAcceptedTurn === "function" && typeof candidate.acceptedTurns === "function"
|
|
404
|
+
? candidate
|
|
405
|
+
: null;
|
|
406
|
+
}
|
|
407
|
+
function initialProgress() {
|
|
408
|
+
return {
|
|
409
|
+
step_advanced: false,
|
|
410
|
+
evidence_added: [],
|
|
411
|
+
artifact_changes: [],
|
|
412
|
+
no_progress: false,
|
|
413
|
+
consecutive_no_progress: 0,
|
|
414
|
+
stagnation: "none",
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function measureProgress(state, before, after) {
|
|
418
|
+
if (!before || !after)
|
|
419
|
+
return null;
|
|
420
|
+
const terminalStatusAdvanced = isTerminalCanonicalStatus(after.canonical_status)
|
|
421
|
+
&& before.canonical_status !== after.canonical_status;
|
|
422
|
+
const stepAdvanced = before.current_step !== after.current_step || terminalStatusAdvanced;
|
|
423
|
+
const evidenceAdded = after.canonical_evidence.filter((entry) => !before.canonical_evidence.includes(entry));
|
|
424
|
+
const artifactChanges = after.observed_artifacts.filter((entry) => !before.observed_artifacts.includes(entry));
|
|
425
|
+
const noProgress = !stepAdvanced && evidenceAdded.length === 0 && artifactChanges.length === 0;
|
|
426
|
+
const consecutiveNoProgress = noProgress ? (state.prior_progress?.consecutive_no_progress ?? 0) + 1 : 0;
|
|
427
|
+
return {
|
|
428
|
+
snapshot: after,
|
|
429
|
+
delta: {
|
|
430
|
+
step_advanced: stepAdvanced,
|
|
431
|
+
evidence_added: evidenceAdded,
|
|
432
|
+
artifact_changes: artifactChanges,
|
|
433
|
+
no_progress: noProgress,
|
|
434
|
+
consecutive_no_progress: consecutiveNoProgress,
|
|
435
|
+
stagnation: consecutiveNoProgress >= 2 ? "stagnant" : consecutiveNoProgress === 1 ? "possible" : "none",
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
}
|
|
355
439
|
function finishDone(store, state, snapshot, now) {
|
|
356
440
|
const done = saveState(store, state, {
|
|
357
441
|
status: "done",
|
|
358
442
|
active_turn_step: null,
|
|
359
443
|
active_turn_public_key_digest: null,
|
|
444
|
+
active_turn_phase: null,
|
|
445
|
+
active_turn_progress: null,
|
|
446
|
+
active_turn_capture: null,
|
|
360
447
|
pending_barrier: null,
|
|
361
448
|
}, now);
|
|
362
449
|
appendEvent(store, done, snapshot, "done", now);
|
|
@@ -367,6 +454,9 @@ function finishFailed(store, state, snapshot, now) {
|
|
|
367
454
|
status: "failed",
|
|
368
455
|
active_turn_step: null,
|
|
369
456
|
active_turn_public_key_digest: null,
|
|
457
|
+
active_turn_phase: null,
|
|
458
|
+
active_turn_progress: null,
|
|
459
|
+
active_turn_capture: null,
|
|
370
460
|
pending_barrier: null,
|
|
371
461
|
}, now);
|
|
372
462
|
return { outcome: "failed", turns_started: failed.turns_started, snapshot };
|
|
@@ -388,155 +478,14 @@ function appendEvent(store, state, snapshot, type, now, extra = {}) {
|
|
|
388
478
|
...(extra.barrier ? { barrier: extra.barrier } : {}),
|
|
389
479
|
...(extra.summary ? { summary: extra.summary } : {}),
|
|
390
480
|
...(extra.failure_kind ? { failure_kind: extra.failure_kind } : {}),
|
|
481
|
+
...(extra.progress ? { progress: structuredClone(extra.progress) } : {}),
|
|
482
|
+
...(extra.turn_id ? { turn_id: extra.turn_id } : {}),
|
|
391
483
|
});
|
|
392
484
|
}
|
|
393
|
-
function validateSnapshot(value) {
|
|
394
|
-
if (!value || typeof value !== "object")
|
|
395
|
-
throw new Error("continuation runtime returned an invalid canonical snapshot");
|
|
396
|
-
for (const field of ["run_id", "definition_id", "status", "current_step"]) {
|
|
397
|
-
if (typeof value[field] !== "string" || value[field].length === 0)
|
|
398
|
-
throw new Error(`continuation snapshot ${field} must be a non-empty string`);
|
|
399
|
-
}
|
|
400
|
-
if (!new Set(["continue", "waiting", "done", "failed"]).has(value.disposition))
|
|
401
|
-
throw new Error("continuation snapshot disposition is invalid");
|
|
402
|
-
if (value.next_action !== null && (typeof value.next_action !== "object" || Array.isArray(value.next_action))) {
|
|
403
|
-
throw new Error("continuation snapshot next_action must be an object or null");
|
|
404
|
-
}
|
|
405
|
-
return structuredClone(value);
|
|
406
|
-
}
|
|
407
|
-
function validateTurnResult(value) {
|
|
408
|
-
if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait")) {
|
|
409
|
-
throw new Error("continuation adapter must return status completed or wait");
|
|
410
|
-
}
|
|
411
|
-
if (value.status === "wait")
|
|
412
|
-
validateBarrier(value.barrier);
|
|
413
|
-
if (value.summary !== undefined && typeof value.summary !== "string")
|
|
414
|
-
throw new Error("continuation adapter summary must be a string");
|
|
415
|
-
if (value.status === "completed" && value.evidence !== undefined)
|
|
416
|
-
validateAdapterEvidence(value.evidence);
|
|
417
|
-
if (value.status === "wait" && Object.hasOwn(value, "evidence"))
|
|
418
|
-
throw new Error("continuation wait results must not include evidence");
|
|
419
|
-
const copy = structuredClone(value);
|
|
420
|
-
if (copy.summary && copy.summary.length > 2_000)
|
|
421
|
-
copy.summary = `${copy.summary.slice(0, 1_997)}...`;
|
|
422
|
-
return copy;
|
|
423
|
-
}
|
|
424
|
-
function validateAdapterEvidence(value) {
|
|
425
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
426
|
-
throw new Error("continuation adapter evidence must be a JSON object");
|
|
427
|
-
}
|
|
428
|
-
let encoded;
|
|
429
|
-
try {
|
|
430
|
-
encoded = JSON.stringify(value);
|
|
431
|
-
}
|
|
432
|
-
catch {
|
|
433
|
-
throw new Error("continuation adapter evidence must be JSON-serializable");
|
|
434
|
-
}
|
|
435
|
-
if (encoded === undefined || Buffer.byteLength(encoded, "utf8") > 65_536) {
|
|
436
|
-
throw new Error("continuation adapter evidence must not exceed 65536 bytes");
|
|
437
|
-
}
|
|
438
|
-
const parsed = JSON.parse(encoded);
|
|
439
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
440
|
-
throw new Error("continuation adapter evidence must be a JSON object");
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
function validateBarrier(barrier) {
|
|
444
|
-
if (!barrier || typeof barrier !== "object")
|
|
445
|
-
throw new Error("continuation wait result requires a barrier");
|
|
446
|
-
if (barrier.kind === "pid") {
|
|
447
|
-
if (!Number.isSafeInteger(barrier.pid) || barrier.pid <= 0)
|
|
448
|
-
throw new Error("continuation pid barrier requires a positive integer pid");
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
if (barrier.kind === "deadline") {
|
|
452
|
-
if (typeof barrier.at !== "string" || !Number.isFinite(Date.parse(barrier.at)))
|
|
453
|
-
throw new Error("continuation deadline barrier requires an ISO date-time");
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
throw new Error("continuation barrier kind must be pid or deadline");
|
|
457
|
-
}
|
|
458
|
-
function validateState(state) {
|
|
459
|
-
if (state.schema_version !== "1.0")
|
|
460
|
-
throw new Error("continuation driver state schema_version must be 1.0");
|
|
461
|
-
assertMaxTurns(state.max_turns);
|
|
462
|
-
if (state.adapter_command_identity !== null && (typeof state.adapter_command_identity !== "string" || state.adapter_command_identity.length === 0)) {
|
|
463
|
-
throw new Error("continuation driver adapter_command_identity must be a non-empty string or null");
|
|
464
|
-
}
|
|
465
|
-
if (!Number.isSafeInteger(state.turns_started) || state.turns_started < 0)
|
|
466
|
-
throw new Error("continuation driver turns_started must be a non-negative integer");
|
|
467
|
-
if (state.turns_started > state.max_turns)
|
|
468
|
-
throw new Error("continuation driver turns_started cannot exceed max_turns");
|
|
469
|
-
if (state.active_turn_step !== undefined && state.active_turn_step !== null && (typeof state.active_turn_step !== "string" || state.active_turn_step.length === 0)) {
|
|
470
|
-
throw new Error("continuation driver active_turn_step must be a non-empty string or null");
|
|
471
|
-
}
|
|
472
|
-
if (state.active_turn_public_key_digest !== undefined && state.active_turn_public_key_digest !== null
|
|
473
|
-
&& (typeof state.active_turn_public_key_digest !== "string" || !/^[a-f0-9]{64}$/.test(state.active_turn_public_key_digest))) {
|
|
474
|
-
throw new Error("continuation driver active_turn_public_key_digest must be a SHA-256 hex digest or null");
|
|
475
|
-
}
|
|
476
|
-
if (!new Set(["active", "waiting", "done", "failed", "budget_exhausted"]).has(state.status))
|
|
477
|
-
throw new Error("continuation driver state status is invalid");
|
|
478
|
-
if (state.status === "budget_exhausted" && state.turns_started !== state.max_turns) {
|
|
479
|
-
throw new Error("continuation driver budget_exhausted state must consume max_turns");
|
|
480
|
-
}
|
|
481
|
-
if (state.pending_barrier)
|
|
482
|
-
validateBarrier(state.pending_barrier);
|
|
483
|
-
}
|
|
484
|
-
function assertMissionIdentity(state, snapshot) {
|
|
485
|
-
if (state.run_id !== snapshot.run_id || state.definition_id !== snapshot.definition_id) {
|
|
486
|
-
throw new Error("continuation driver mission identity does not match the canonical Flow run");
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
function assertMaxTurns(value) {
|
|
490
|
-
if (!Number.isSafeInteger(value) || value < 1 || value > 100)
|
|
491
|
-
throw new Error("continuation maxTurns must be an integer from 1 through 100");
|
|
492
|
-
}
|
|
493
485
|
function boundedErrorMessage(error) {
|
|
494
486
|
const message = error instanceof Error ? error.message : String(error);
|
|
495
487
|
return message.length <= 2_000 ? message : `${message.slice(0, 1_997)}...`;
|
|
496
488
|
}
|
|
497
|
-
function processAlive(pid) {
|
|
498
|
-
try {
|
|
499
|
-
process.kill(pid, 0);
|
|
500
|
-
return true;
|
|
501
|
-
}
|
|
502
|
-
catch (error) {
|
|
503
|
-
return error.code === "EPERM";
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
function reconcileStateWithEvents(state, eventsFile) {
|
|
507
|
-
if (!fs.existsSync(eventsFile)) {
|
|
508
|
-
if (state.turns_started > 0)
|
|
509
|
-
throw new Error("continuation driver event history is missing for a started mission");
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
const lines = readRegularFileNoFollow(eventsFile, "continuation driver event log").split("\n").filter((line) => line.length > 0);
|
|
513
|
-
let highestStartedTurn = 0;
|
|
514
|
-
let eventBarrier = null;
|
|
515
|
-
for (const line of lines) {
|
|
516
|
-
const event = JSON.parse(line);
|
|
517
|
-
if (event.run_id !== state.run_id || event.definition_id !== state.definition_id)
|
|
518
|
-
throw new Error("continuation driver event history has a foreign mission identity");
|
|
519
|
-
if (event.type === "turn_started") {
|
|
520
|
-
if (!Number.isSafeInteger(event.turns_started) || event.turns_started < 1)
|
|
521
|
-
throw new Error("continuation driver event history has an invalid turn count");
|
|
522
|
-
highestStartedTurn = Math.max(highestStartedTurn, event.turns_started);
|
|
523
|
-
}
|
|
524
|
-
if (event.type === "parked") {
|
|
525
|
-
validateBarrier(event.barrier);
|
|
526
|
-
eventBarrier = structuredClone(event.barrier);
|
|
527
|
-
}
|
|
528
|
-
if (event.type === "resumed")
|
|
529
|
-
eventBarrier = null;
|
|
530
|
-
}
|
|
531
|
-
if (highestStartedTurn > state.turns_started)
|
|
532
|
-
throw new Error("continuation driver state rolled back behind its event history");
|
|
533
|
-
if (state.turns_started > highestStartedTurn + 1)
|
|
534
|
-
throw new Error("continuation driver state is ahead of its event history");
|
|
535
|
-
if (!state.pending_barrier && eventBarrier) {
|
|
536
|
-
state.pending_barrier = eventBarrier;
|
|
537
|
-
state.status = "waiting";
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
489
|
function builderContinuationDisposition(nextAction) {
|
|
541
490
|
if (!nextAction || typeof nextAction !== "object" || Array.isArray(nextAction))
|
|
542
491
|
throw new Error("Builder Flow projection is missing its canonical next action");
|