@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/build/src/builder-flow-runtime.js +118 -17
  3. package/build/src/cli/assignment-provider.js +13 -1
  4. package/build/src/cli/continuation-adapter.d.ts +24 -0
  5. package/build/src/cli/continuation-adapter.js +225 -0
  6. package/build/src/cli/workflow-sidecar.js +29 -11
  7. package/build/src/cli/workflow.js +67 -11
  8. package/build/src/continuation-driver.d.ts +92 -0
  9. package/build/src/continuation-driver.js +444 -0
  10. package/build/src/index.d.ts +2 -0
  11. package/build/src/index.js +1 -0
  12. package/build/src/tools/build-universal-bundles.js +53 -3
  13. package/docs/getting-started.md +9 -1
  14. package/docs/public-workflow-cli.md +54 -0
  15. package/docs/spec/builder-flow-runtime.md +36 -2
  16. package/docs/workflow-usage-guide.md +7 -0
  17. package/evals/integration/test_builder_step_producers.sh +37 -4
  18. package/evals/integration/test_bundle_install.sh +108 -4
  19. package/evals/integration/test_bundle_lifecycle.sh +4 -6
  20. package/evals/integration/test_install_merge.sh +18 -8
  21. package/evals/integration/test_public_workflow_cli.sh +11 -5
  22. package/evals/static/test_universal_bundles.sh +44 -1
  23. package/package.json +4 -4
  24. package/packaging/README.md +1 -1
  25. package/scripts/README.md +1 -1
  26. package/scripts/install-codex-home.sh +63 -23
  27. package/scripts/install-owned-files.js +18 -2
  28. package/src/builder-flow-runtime.ts +108 -10
  29. package/src/cli/assignment-provider.ts +13 -1
  30. package/src/cli/builder-flow-runtime.test.mjs +378 -20
  31. package/src/cli/continuation-adapter.ts +239 -0
  32. package/src/cli/continuation-driver.test.mjs +564 -0
  33. package/src/cli/workflow-sidecar.ts +27 -11
  34. package/src/cli/workflow.ts +68 -11
  35. package/src/continuation-driver.ts +532 -0
  36. package/src/index.ts +20 -0
  37. 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 = manifestEvidenceDigests(repaired.run.manifest);
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 = manifestEvidenceDigests(run.manifest);
215
- const newEvidence = afterEvidence.filter((value) => !beforeEvidence.includes(value));
216
- if (newEvidence.length !== 1 || newEvidence[0] !== digest) {
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: true,
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(`Recorded evidence; canonical run is ${report.status} at ${report.current_step}.`);
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 manifestEvidenceDigests(manifest) {
586
+ function manifestEvidenceIdentity(manifest) {
545
587
  const evidence = Array.isArray(manifest.evidence) ? manifest.evidence : [];
546
588
  return evidence
547
- .map((entry) => entry && typeof entry === "object" ? entry.sha256 : null)
548
- .filter((digest) => typeof digest === "string");
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;