@kontourai/flow-agents 3.7.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 +19 -0
- package/build/src/builder-flow-runtime.js +118 -17
- package/build/src/cli/assignment-provider.js +13 -1
- package/build/src/cli/continuation-adapter.d.ts +24 -0
- package/build/src/cli/continuation-adapter.js +225 -0
- package/build/src/cli/workflow-sidecar.js +29 -11
- package/build/src/cli/workflow.js +67 -11
- package/build/src/continuation-driver.d.ts +92 -0
- package/build/src/continuation-driver.js +444 -0
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/tools/build-universal-bundles.js +53 -3
- package/docs/getting-started.md +9 -1
- package/docs/public-workflow-cli.md +54 -0
- package/docs/spec/builder-flow-runtime.md +36 -2
- package/docs/workflow-usage-guide.md +7 -0
- package/evals/integration/test_builder_step_producers.sh +37 -4
- package/evals/integration/test_bundle_install.sh +108 -4
- package/evals/integration/test_bundle_lifecycle.sh +4 -6
- package/evals/integration/test_install_merge.sh +18 -8
- package/evals/integration/test_public_workflow_cli.sh +11 -5
- package/evals/static/test_universal_bundles.sh +44 -1
- package/package.json +4 -4
- package/packaging/README.md +1 -1
- package/scripts/README.md +1 -1
- package/scripts/install-codex-home.sh +63 -23
- package/scripts/install-owned-files.js +18 -2
- package/src/builder-flow-runtime.ts +108 -10
- package/src/cli/assignment-provider.ts +13 -1
- package/src/cli/builder-flow-runtime.test.mjs +378 -20
- package/src/cli/continuation-adapter.ts +239 -0
- package/src/cli/continuation-driver.test.mjs +564 -0
- package/src/cli/workflow-sidecar.ts +27 -11
- package/src/cli/workflow.ts +68 -11
- package/src/continuation-driver.ts +532 -0
- package/src/index.ts +20 -0
- package/src/tools/build-universal-bundles.ts +51 -3
|
@@ -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");
|
|
@@ -200,7 +235,7 @@ async function evidence(sessionDir, argv, json) {
|
|
|
200
235
|
// session state cannot change mid-invocation.
|
|
201
236
|
const caller = assertMatchingAssignmentActor(sessionDir, slug);
|
|
202
237
|
const repaired = await recoverBuilderFlowSession({ sessionDir });
|
|
203
|
-
const beforeEvidence =
|
|
238
|
+
const beforeEvidence = manifestEvidenceIdentity(repaired.run.manifest);
|
|
204
239
|
await mainFromPublicWorkflow([
|
|
205
240
|
"record-gate-claim",
|
|
206
241
|
sessionDir,
|
|
@@ -208,27 +243,34 @@ async function evidence(sessionDir, argv, json) {
|
|
|
208
243
|
"--actor",
|
|
209
244
|
caller.actorKey,
|
|
210
245
|
]);
|
|
211
|
-
await syncBuilderFlowSession({ sessionDir });
|
|
246
|
+
const synchronized = await syncBuilderFlowSession({ sessionDir });
|
|
212
247
|
const digest = fileSha256(path.join(sessionDir, "trust.bundle"));
|
|
213
248
|
const run = await loadBuilderFlowRun({ cwd: repaired.projectRoot, runId: slug });
|
|
214
|
-
const afterEvidence =
|
|
215
|
-
const
|
|
216
|
-
|
|
249
|
+
const afterEvidence = manifestEvidenceIdentity(run.manifest);
|
|
250
|
+
const beforeIds = new Set(beforeEvidence.map((entry) => entry.id));
|
|
251
|
+
const newEvidence = afterEvidence.filter((entry) => !beforeIds.has(entry.id));
|
|
252
|
+
if (synchronized.attached && (newEvidence.length !== 1 || newEvidence[0]?.sha256 !== digest)) {
|
|
217
253
|
throw new Error("workflow evidence did not attach exactly this invocation's resulting trust.bundle digest");
|
|
218
254
|
}
|
|
255
|
+
if (!synchronized.attached && newEvidence.length !== 0) {
|
|
256
|
+
throw new Error("workflow evidence changed the canonical manifest while synchronization reported no attachment");
|
|
257
|
+
}
|
|
219
258
|
const updatedSidecar = readBoundSession(sessionDir).sidecar;
|
|
220
259
|
return immutableReport({
|
|
221
260
|
run_id: run.runId,
|
|
222
261
|
status: run.state.status,
|
|
223
262
|
current_step: run.state.current_step,
|
|
224
|
-
attached:
|
|
263
|
+
attached: synchronized.attached,
|
|
264
|
+
awaiting_evidence: !synchronized.attached,
|
|
225
265
|
next_action: updatedSidecar.next_action ?? null,
|
|
226
266
|
});
|
|
227
267
|
});
|
|
228
268
|
if (json)
|
|
229
269
|
console.log(JSON.stringify(report));
|
|
230
270
|
else
|
|
231
|
-
console.log(
|
|
271
|
+
console.log(report.attached
|
|
272
|
+
? `Recorded evidence; canonical run is ${report.status} at ${report.current_step}.`
|
|
273
|
+
: `Recorded evidence; canonical run is awaiting the remaining gate expectations at ${report.current_step}.`);
|
|
232
274
|
return 0;
|
|
233
275
|
}
|
|
234
276
|
async function critique(sessionDir, argv, json) {
|
|
@@ -541,11 +583,14 @@ function immutableReport(value) {
|
|
|
541
583
|
immutableReport(nested);
|
|
542
584
|
return Object.freeze(value);
|
|
543
585
|
}
|
|
544
|
-
function
|
|
586
|
+
function manifestEvidenceIdentity(manifest) {
|
|
545
587
|
const evidence = Array.isArray(manifest.evidence) ? manifest.evidence : [];
|
|
546
588
|
return evidence
|
|
547
|
-
.
|
|
548
|
-
|
|
589
|
+
.flatMap((entry) => entry && typeof entry === "object"
|
|
590
|
+
&& typeof entry.id === "string"
|
|
591
|
+
&& typeof entry.sha256 === "string"
|
|
592
|
+
? [{ id: String(entry.id), sha256: String(entry.sha256) }]
|
|
593
|
+
: []);
|
|
549
594
|
}
|
|
550
595
|
function optionalFileDigest(file) {
|
|
551
596
|
try {
|
|
@@ -600,6 +645,17 @@ function assertOnlyFlags(flags, allowed, command) {
|
|
|
600
645
|
if (unsupported)
|
|
601
646
|
throw new Error(`${command} does not support --${unsupported}`);
|
|
602
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
|
+
}
|
|
603
659
|
function isWithin(candidate, root) {
|
|
604
660
|
const relative = path.relative(root, candidate);
|
|
605
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;
|