ai-data-workflow-logic 0.1.11 → 0.1.14

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/src/filesystem.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import type {
4
+ AIDataWorkflowChildInvocationIdentity,
5
+ AIDataWorkflowChildInvocationRuntimePort,
6
+ AIDataWorkflowChildTerminalReceipt,
4
7
  AIDataWorkflowRunCheckpoint,
5
8
  AIDataWorkflowRunGraph,
6
9
  } from 'ai-data-workflow-contract';
@@ -12,10 +15,13 @@ import type {
12
15
  AIWorkflowReusePolicy,
13
16
  FlowClosedValue,
14
17
  } from 'ai-workflow-contract';
18
+ import type { EagerDataFlowModuleImporter } from 'eager-data-flow-logic';
15
19
  import type {
16
- EagerDataFlowModuleImporter,
17
- } from 'eager-data-flow-logic';
18
- import type { EagerDataFlowDurableNodeState } from 'eager-data-flow-contract';
20
+ EagerDataFlowDurableNodeState,
21
+ EagerDataFlowExecutable,
22
+ EagerDataFlowExecutableRegistry,
23
+ EagerDataFlowRecord,
24
+ } from 'eager-data-flow-contract';
19
25
  import {
20
26
  createFilesystemCodeResolver,
21
27
  materializeEagerDataFlowPlan,
@@ -30,6 +36,7 @@ import {
30
36
  } from 'ai-workflow-logic/checkpoint-filesystem';
31
37
  import {
32
38
  createInitialRunStepExtensions,
39
+ FlowRunCheckpointVersionConflictError,
33
40
  normalizeAIWorkflowProfileDurableState,
34
41
  normalizeFlowClosedObject,
35
42
  normalizeFlowClosedValue,
@@ -45,14 +52,27 @@ import {
45
52
  } from 'work-ctrl-flow-logic';
46
53
  import {
47
54
  commitAIDataWorkflowCheckpoint,
55
+ acceptAIDataWorkflowChildTerminalReceipt,
56
+ createAIDataWorkflowChildInvocationIdentity,
48
57
  createAIDataWorkflowCheckpoint,
49
58
  createAIDataWorkflowRunGraph,
50
59
  createAIDataWorkflowAgentNodeRuntimeBinder,
51
60
  loadAIDataWorkflowCheckpoint,
52
61
  loadAIDataWorkflowSources,
62
+ normalizeAIDataWorkflowChildInvocationObservation,
63
+ normalizeAIDataWorkflowChildTerminalReceipt,
64
+ projectAIDataWorkflowRunGraphPlan,
53
65
  recordAIDataWorkflowNodeResult,
66
+ recordAIDataWorkflowChildFreezeReceipt,
67
+ reserveAIDataWorkflowChildInvocation,
54
68
  } from './index.js';
55
69
 
70
+ export interface MaterializedAIDataWorkflowChildFlow {
71
+ readonly definitionRef?: `resource://${string}`;
72
+ readonly contractDigest?: string;
73
+ readonly executable: EagerDataFlowExecutable<any>;
74
+ }
75
+
56
76
  export interface MaterializedAIDataWorkflowRuntime {
57
77
  readonly supportRoot: string;
58
78
  readonly workflow: AIDataWorkflowRuntime;
@@ -60,6 +80,15 @@ export interface MaterializedAIDataWorkflowRuntime {
60
80
  readonly extensionCodecs?: DefinitionStepAssemblyRuntime['extensionCodecs'];
61
81
  readonly agentEffects?: AIAgentHostEffectPort;
62
82
  readonly agentTaskProofs?: Readonly<Record<string, FrozenAIAgentTaskBinding>>;
83
+ /** Exact admitted child definitions keyed by their eager-data-flow id. */
84
+ readonly childFlows?: Readonly<Record<string, MaterializedAIDataWorkflowChildFlow>>;
85
+ readonly childInvocations?: AIDataWorkflowChildInvocationRuntimePort;
86
+ /** Read-only lifecycle observation/fault-injection seam; it owns no parent or child facts. */
87
+ readonly childInvocationObserver?: (event: {
88
+ readonly phase: 'settled-before-parent-accept';
89
+ readonly identity: AIDataWorkflowChildInvocationIdentity;
90
+ readonly receipt: AIDataWorkflowChildTerminalReceipt;
91
+ }) => void | Promise<void>;
63
92
  readonly checkpointObserver?: (event: {
64
93
  readonly checkpoint: AIDataWorkflowRunCheckpoint;
65
94
  readonly nodeId: string;
@@ -209,43 +238,56 @@ async function executeAIDataCheckpoint(
209
238
  nodeId: string,
210
239
  phase: 'before' | 'after' | 'failed',
211
240
  ): Promise<void> => transition(async () => {
212
- const owner = await loadAIDataWorkflowCheckpoint(checkpointRuntime, {
213
- instanceId: checkpoint.instanceId,
214
- runId: checkpoint.runId,
215
- });
216
- if (!owner) throw new Error(`AI Data Workflow checkpoint ${checkpoint.runId} disappeared during node transition.`);
217
- accepted = owner;
218
- graph = { ...owner.profile.runGraph, binding };
219
- const nextGraph = updateGraph(graph);
220
- const nextStatus = graphCheckpointStatus(nextGraph);
221
- accepted = await commitAIDataWorkflowCheckpoint(checkpointRuntime, {
222
- expectedVersion: accepted.version,
223
- checkpoint: {
224
- ...accepted,
225
- version: accepted.version + 1,
226
- state: { status: nextStatus, generation: nextGraph.currentGeneration },
227
- output: graphCheckpointOutput(nextGraph, nextStatus),
228
- controllerSidecars: { status: nextStatus },
229
- nodeSidecars: nodeStatuses(nextGraph),
230
- profile: { ...accepted.profile, runGraph: durableGraph(nextGraph) },
231
- },
232
- });
233
- graph = { ...accepted.profile.runGraph, binding };
234
- if (runtime.checkpointObserver) {
241
+ for (let attempt = 0; attempt < 8; attempt += 1) {
242
+ const owner = await loadAIDataWorkflowCheckpoint(checkpointRuntime, {
243
+ instanceId: checkpoint.instanceId,
244
+ runId: checkpoint.runId,
245
+ });
246
+ if (!owner) throw new Error(`AI Data Workflow checkpoint ${checkpoint.runId} disappeared during node transition.`);
247
+ accepted = owner;
248
+ graph = { ...owner.profile.runGraph, binding };
249
+ const nextGraph = updateGraph(graph);
250
+ if (nextGraph === graph) return;
251
+ const nextStatus = graphCheckpointStatus(nextGraph);
235
252
  try {
236
- await runtime.checkpointObserver({ checkpoint: accepted, nodeId, phase });
253
+ accepted = await commitAIDataWorkflowCheckpoint(checkpointRuntime, {
254
+ expectedVersion: accepted.version,
255
+ checkpoint: {
256
+ ...accepted,
257
+ version: accepted.version + 1,
258
+ state: { status: nextStatus, generation: nextGraph.currentGeneration },
259
+ output: graphCheckpointOutput(nextGraph, nextStatus),
260
+ controllerSidecars: { status: nextStatus },
261
+ nodeSidecars: nodeStatuses(nextGraph),
262
+ profile: { ...accepted.profile, runGraph: durableGraph(nextGraph) },
263
+ },
264
+ });
237
265
  } catch (cause) {
238
- throw new AIDataCheckpointObserverError(cause);
266
+ if (cause instanceof FlowRunCheckpointVersionConflictError) continue;
267
+ throw cause;
239
268
  }
269
+ graph = { ...accepted.profile.runGraph, binding };
270
+ if (runtime.checkpointObserver) {
271
+ try {
272
+ await runtime.checkpointObserver({ checkpoint: accepted, nodeId, phase });
273
+ } catch (cause) {
274
+ throw new AIDataCheckpointObserverError(cause);
275
+ }
276
+ }
277
+ return;
240
278
  }
279
+ throw new Error(`AI Data Workflow node ${nodeId} checkpoint CAS did not converge.`);
241
280
  });
242
281
  let output: unknown;
243
282
  try {
244
- const executable = await materializeEagerDataFlowPlan(binding.definition, {
283
+ const executionPlan = projectAIDataWorkflowRunGraphPlan(graph);
284
+ const executable = await materializeEagerDataFlowPlan(executionPlan, {
245
285
  resolveCode: createFilesystemCodeResolver(runtime.importModule),
286
+ subflows: createMaterializedChildRegistry(runtime, () => graph),
246
287
  });
247
288
  output = await executable.invoke(runtime.workflow, invocationInput, {
248
289
  completedNodes: completedEagerNodes(graph),
290
+ invokeSubFlow: createMaterializedChildInvoker(runtime, () => graph, commitGraph),
249
291
  ...(runtime.agentEffects ? {
250
292
  bindNode: ((binder) => (nodeRuntime: unknown, identity: { readonly nodeId: string }) => binder(
251
293
  nodeRuntime as AIDataWorkflowRuntime,
@@ -264,6 +306,14 @@ async function executeAIDataCheckpoint(
264
306
  return commitGraph((current) => {
265
307
  const node = current.nodes[nodeId];
266
308
  if (!node) throw new Error(`AI Data Workflow node ${nodeId} does not exist in the durable graph.`);
309
+ if (node.tag === 'SubFlowNode' && node.subflow?.kind === 'autonomous') {
310
+ const identity = node.childInvocation ?? createAIDataWorkflowChildInvocationIdentity({
311
+ graph: current,
312
+ parentInstanceId: checkpoint.instanceId,
313
+ nodeId,
314
+ });
315
+ return reserveAIDataWorkflowChildInvocation({ graph: current, identity });
316
+ }
267
317
  return recordAIDataWorkflowNodeResult(current, nodeId, {
268
318
  nodeId,
269
319
  generation: node.generation,
@@ -275,6 +325,7 @@ async function executeAIDataCheckpoint(
275
325
  return commitGraph((current) => {
276
326
  const node = current.nodes[settlement.nodeId];
277
327
  if (!node) throw new Error(`AI Data Workflow node ${settlement.nodeId} does not exist in the durable graph.`);
328
+ if (node.tag === 'SubFlowNode' && node.childTerminalReceipt) return current;
278
329
  return recordAIDataWorkflowNodeResult(current, settlement.nodeId, {
279
330
  nodeId: settlement.nodeId,
280
331
  generation: node.generation,
@@ -284,10 +335,13 @@ async function executeAIDataCheckpoint(
284
335
  });
285
336
  }, settlement.nodeId, 'after');
286
337
  },
287
- onNodeFailure: ({ nodeId }) => {
338
+ onNodeFailure: ({ nodeId, cause }) => {
288
339
  return commitGraph((current) => {
289
340
  const node = current.nodes[nodeId];
290
341
  if (!node) throw new Error(`AI Data Workflow node ${nodeId} does not exist in the durable graph.`);
342
+ if (cause instanceof AIDataChildInvocationSuspendedError
343
+ || cause instanceof AIDataChildInvocationTerminalFailure
344
+ || findCheckpointObserverError(cause)) return current;
291
345
  return recordAIDataWorkflowNodeResult(current, nodeId, {
292
346
  nodeId,
293
347
  generation: node.generation,
@@ -310,6 +364,125 @@ async function executeAIDataCheckpoint(
310
364
  return { checkpoint: accepted, graph };
311
365
  }
312
366
 
367
+ class AIDataChildInvocationSuspendedError extends Error {
368
+ constructor(nodeId: string, status: string) {
369
+ super(`AI Data child invocation ${nodeId} is ${status}; continue the parent checkpoint to resume it.`);
370
+ this.name = 'AIDataChildInvocationSuspendedError';
371
+ }
372
+ }
373
+
374
+ class AIDataChildInvocationTerminalFailure extends Error {
375
+ constructor(nodeId: string, code: string, message: string) {
376
+ super(`AI Data child invocation ${nodeId} failed (${code}): ${message}`);
377
+ this.name = 'AIDataChildInvocationTerminalFailure';
378
+ }
379
+ }
380
+
381
+ function createMaterializedChildRegistry(
382
+ runtime: MaterializedAIDataWorkflowRuntime,
383
+ currentGraph: () => AIDataWorkflowRunGraph,
384
+ ): EagerDataFlowExecutableRegistry<any> {
385
+ const registry: Record<string, EagerDataFlowExecutable<any>> = {};
386
+ for (const nodeId of currentGraph().declarationOrder) {
387
+ const node = currentGraph().nodes[nodeId];
388
+ if (!node || node.status === 'Removed' || node.tag !== 'SubFlowNode' || !node.subflow) continue;
389
+ const flowId = node.subflow.flowRef.slice('eager-data-flow://'.length);
390
+ const admitted = runtime.childFlows?.[flowId];
391
+ if (!admitted || admitted.executable.plan.fqn !== flowId) {
392
+ throw new Error(`AI Data child flow ${flowId} is not present in the admitted executable registry.`);
393
+ }
394
+ if (node.subflow.kind === 'autonomous' && (admitted.definitionRef !== node.subflow.definitionRef
395
+ || admitted.contractDigest !== node.subflow.contractDigest)) {
396
+ throw new Error(`AI Data child flow ${flowId} does not match its frozen definition and contract.`);
397
+ }
398
+ if (node.subflow.kind === 'autonomous' && !runtime.childInvocations) {
399
+ throw new Error(`AI Data autonomous child flow ${flowId} requires a child invocation runtime port.`);
400
+ }
401
+ registry[flowId] = admitted.executable;
402
+ }
403
+ return Object.freeze(registry);
404
+ }
405
+
406
+ function createMaterializedChildInvoker(
407
+ runtime: MaterializedAIDataWorkflowRuntime,
408
+ currentGraph: () => AIDataWorkflowRunGraph,
409
+ commitGraph: (
410
+ updateGraph: (current: AIDataWorkflowRunGraph) => AIDataWorkflowRunGraph,
411
+ nodeId: string,
412
+ phase: 'before' | 'after' | 'failed',
413
+ ) => Promise<void>,
414
+ ) {
415
+ return async (
416
+ workflow: AIDataWorkflowRuntime,
417
+ invocation: { readonly nodeId: string; readonly flowRef: string },
418
+ input: Readonly<EagerDataFlowRecord>,
419
+ options: { readonly signal: AbortSignal },
420
+ ): Promise<EagerDataFlowRecord> => {
421
+ const node = currentGraph().nodes[invocation.nodeId];
422
+ if (!node || node.tag !== 'SubFlowNode' || node.subflow?.flowRef !== invocation.flowRef) {
423
+ throw new Error(`AI Data child dispatch ${invocation.nodeId} does not match the current RunGraph.`);
424
+ }
425
+ const flowId = invocation.flowRef.slice('eager-data-flow://'.length);
426
+ const admitted = runtime.childFlows?.[flowId];
427
+ if (!admitted) throw new Error(`AI Data child flow ${flowId} is not admitted.`);
428
+ if (node.subflow.kind === 'declared') {
429
+ return admitted.executable.invoke(workflow, input, { signal: options.signal });
430
+ }
431
+ if (!node.childInvocation || !runtime.childInvocations) {
432
+ throw new Error(`AI Data child node ${invocation.nodeId} was not reserved before dispatch.`);
433
+ }
434
+ const identity = node.childInvocation;
435
+ let freeze = node.childFreezeReceipt;
436
+ if (!freeze) {
437
+ freeze = await runtime.childInvocations.resolveAndFreeze({ identity });
438
+ await commitGraph(
439
+ (graph) => recordAIDataWorkflowChildFreezeReceipt({ graph, nodeId: node.id, receipt: freeze! }),
440
+ node.id,
441
+ 'before',
442
+ );
443
+ freeze = currentGraph().nodes[node.id].childFreezeReceipt;
444
+ }
445
+ if (!freeze) throw new Error(`AI Data child node ${node.id} freeze receipt was not committed.`);
446
+ let observation = normalizeAIDataWorkflowChildInvocationObservation(
447
+ await runtime.childInvocations.startOrContinue({
448
+ identity,
449
+ freeze,
450
+ input: normalizeFlowClosedObject(input, `aiData.child.${node.id}.input`),
451
+ }),
452
+ identity,
453
+ );
454
+ if (observation.status === 'Reserved' || observation.status === 'Running') {
455
+ observation = normalizeAIDataWorkflowChildInvocationObservation(
456
+ await runtime.childInvocations.load({ identity, freeze }),
457
+ identity,
458
+ );
459
+ }
460
+ if (observation.status === 'Reserved' || observation.status === 'Running') {
461
+ throw new AIDataChildInvocationSuspendedError(node.id, observation.status);
462
+ }
463
+ const receipt = normalizeAIDataWorkflowChildTerminalReceipt(
464
+ await runtime.childInvocations.settle({ identity, freeze, terminalStatus: observation.status }),
465
+ identity,
466
+ );
467
+ if (runtime.childInvocationObserver) {
468
+ try {
469
+ await runtime.childInvocationObserver({ phase: 'settled-before-parent-accept', identity, receipt });
470
+ } catch (cause) {
471
+ throw new AIDataCheckpointObserverError(cause);
472
+ }
473
+ }
474
+ await commitGraph(
475
+ (graph) => acceptAIDataWorkflowChildTerminalReceipt({ graph, nodeId: node.id, receipt }),
476
+ node.id,
477
+ receipt.status === 'Succeeded' ? 'after' : 'failed',
478
+ );
479
+ if (receipt.status === 'Failed') {
480
+ throw new AIDataChildInvocationTerminalFailure(node.id, receipt.failureCode, receipt.failureMessage);
481
+ }
482
+ return receipt.output as EagerDataFlowRecord;
483
+ };
484
+ }
485
+
313
486
  class AIDataCheckpointObserverError extends Error {
314
487
  constructor(public override readonly cause: unknown) {
315
488
  super('AI Data checkpoint observer interrupted execution.');
package/src/index.ts CHANGED
@@ -11,6 +11,9 @@ import type {
11
11
  AIDataWorkflowRunNode,
12
12
  AIDataWorkflowRunNodeStatus,
13
13
  AIDataWorkflowSemanticInput,
14
+ AIDataWorkflowSubFlowBinding,
15
+ AIDataWorkflowChildInvocationIdentity,
16
+ AIDataWorkflowChildTerminalReceipt,
14
17
  } from 'ai-data-workflow-contract';
15
18
  import type {
16
19
  AIDataWorkflowDefinitionBinding,
@@ -62,6 +65,8 @@ import {
62
65
  import { assertFrozenAIAgentTaskBinding } from 'ai-workflow-logic/run-freeze';
63
66
 
64
67
  export * from './holon-task.js';
68
+ export * from './autonomous-control.js';
69
+ export * from './child-invocation.js';
65
70
 
66
71
  export interface CreateAIDataWorkflowCheckpointInput {
67
72
  readonly instanceId: string;
@@ -286,12 +291,20 @@ function cloneInvalidation(invalidation: AIDataWorkflowInvalidation): AIDataWork
286
291
  return { ...invalidation };
287
292
  }
288
293
 
294
+ function cloneSubflow(binding: AIDataWorkflowSubFlowBinding): AIDataWorkflowSubFlowBinding {
295
+ return { ...binding };
296
+ }
297
+
289
298
  function cloneNode(node: AIDataWorkflowRunNode): AIDataWorkflowRunNode {
290
299
  return {
291
300
  ...node,
292
301
  inputs: cloneRecord(node.inputs),
293
302
  outputs: [...node.outputs],
294
303
  config: cloneRecord(node.config),
304
+ ...(node.subflow ? { subflow: cloneSubflow(node.subflow) } : {}),
305
+ ...(node.childInvocation ? { childInvocation: cloneChildInvocation(node.childInvocation) } : {}),
306
+ ...(node.childFreezeReceipt ? { childFreezeReceipt: { ...node.childFreezeReceipt } } : {}),
307
+ ...(node.childTerminalReceipt ? { childTerminalReceipt: cloneChildTerminalReceipt(node.childTerminalReceipt) } : {}),
295
308
  dependsOn: [...node.dependsOn],
296
309
  dependents: [...node.dependents],
297
310
  reusePolicy: { ...node.reusePolicy },
@@ -301,6 +314,16 @@ function cloneNode(node: AIDataWorkflowRunNode): AIDataWorkflowRunNode {
301
314
  };
302
315
  }
303
316
 
317
+ function cloneChildInvocation(identity: AIDataWorkflowChildInvocationIdentity): AIDataWorkflowChildInvocationIdentity {
318
+ return { ...identity, subflow: { ...identity.subflow } };
319
+ }
320
+
321
+ function cloneChildTerminalReceipt(receipt: AIDataWorkflowChildTerminalReceipt): AIDataWorkflowChildTerminalReceipt {
322
+ return receipt.status === 'Succeeded'
323
+ ? { ...receipt, output: { ...receipt.output } }
324
+ : { ...receipt };
325
+ }
326
+
304
327
  function cloneNodeResult(result: AIWorkflowNodeResult): AIWorkflowNodeResult {
305
328
  return {
306
329
  ...result,
@@ -325,7 +348,27 @@ function cloneGraph(graph: AIDataWorkflowRunGraph): AIDataWorkflowRunGraph {
325
348
  declarationOrder: [...graph.declarationOrder],
326
349
  patchHistory: graph.patchHistory.map((record) => ({
327
350
  ...record,
328
- operations: record.operations.map((operation) => ({ ...operation })),
351
+ operations: record.operations.map((operation) => {
352
+ if (operation.op === 'add-node') {
353
+ return {
354
+ ...operation,
355
+ node: {
356
+ ...operation.node,
357
+ ...(operation.node.subflow ? { subflow: cloneSubflow(operation.node.subflow) } : {}),
358
+ },
359
+ };
360
+ }
361
+ if (operation.op === 'update-node') {
362
+ return {
363
+ ...operation,
364
+ changes: {
365
+ ...operation.changes,
366
+ ...(operation.changes.subflow ? { subflow: cloneSubflow(operation.changes.subflow) } : {}),
367
+ },
368
+ };
369
+ }
370
+ return { ...operation };
371
+ }),
329
372
  })),
330
373
  invalidations: graph.invalidations.map(cloneInvalidation),
331
374
  };
@@ -353,6 +396,40 @@ function nodeConfig(node: EagerDataFlowNodePlan): AIDataWorkflowNodeConfig {
353
396
  return {};
354
397
  }
355
398
 
399
+ function nodeSubflow(node: EagerDataFlowNodePlan): AIDataWorkflowSubFlowBinding | undefined {
400
+ if (node.tag !== 'SubFlowNode') return undefined;
401
+ return {
402
+ kind: 'declared',
403
+ flowRef: node.flow as `eager-data-flow://${string}`,
404
+ };
405
+ }
406
+
407
+ function assertSubflowBinding(
408
+ tag: AIDataWorkflowNodeTag,
409
+ binding: AIDataWorkflowSubFlowBinding | undefined,
410
+ nodeId: string,
411
+ ): void {
412
+ if (tag === 'SubFlowNode' && !binding) {
413
+ throw new AIDataWorkflowRunGraphError(`AI Data Workflow SubFlowNode ${nodeId} requires an exact subflow binding`);
414
+ }
415
+ if (tag !== 'SubFlowNode' && binding) {
416
+ throw new AIDataWorkflowRunGraphError(`AI Data Workflow node ${nodeId} cannot carry a subflow binding`);
417
+ }
418
+ if (!binding) return;
419
+ if (!binding.flowRef.startsWith('eager-data-flow://') || binding.flowRef.length === 'eager-data-flow://'.length) {
420
+ throw new AIDataWorkflowRunGraphError(`AI Data Workflow SubFlowNode ${nodeId} has an invalid flow reference`);
421
+ }
422
+ if (binding.kind === 'autonomous' && (
423
+ binding.schemaVersion !== 'depa.ai-data-subflow-binding/v1'
424
+ || !binding.capabilityId
425
+ || !binding.catalogDigest
426
+ || !binding.contractDigest
427
+ || !binding.definitionRef.startsWith('resource://')
428
+ )) {
429
+ throw new AIDataWorkflowRunGraphError(`AI Data Workflow SubFlowNode ${nodeId} has an invalid autonomous binding`);
430
+ }
431
+ }
432
+
356
433
  function configReusePolicy(config: AIDataWorkflowNodeConfig): AIWorkflowReusePolicy | undefined {
357
434
  const raw = config.reuse_policy ?? config.reusePolicy;
358
435
  return raw === 'semantic-hash' || raw === 'never' ? raw : undefined;
@@ -547,10 +624,12 @@ export function createAIDataWorkflowRunGraph(
547
624
  inputs: nodeInputs(planNode),
548
625
  outputs: nodeOutputs(planNode),
549
626
  config,
627
+ ...(nodeSubflow(planNode) ? { subflow: nodeSubflow(planNode) } : {}),
550
628
  dependsOn: dependencies.get(planNode.id) ?? [],
551
629
  dependents: [],
552
630
  reusePolicy,
553
631
  };
632
+ assertSubflowBinding(runNode.tag, runNode.subflow, runNode.id);
554
633
  return [planNode.id, runNode];
555
634
  }),
556
635
  );
@@ -573,6 +652,96 @@ export function createAIDataWorkflowRunGraph(
573
652
  return graph;
574
653
  }
575
654
 
655
+ function isRunGraphPortReference(value: AIDataWorkflowNodeInputs[string]): value is import('eager-data-flow-contract').FlowPortReference {
656
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value)
657
+ && typeof (value as { ref?: unknown }).ref === 'string'
658
+ && typeof (value as { nodeId?: unknown }).nodeId === 'string'
659
+ && typeof (value as { port?: unknown }).port === 'string');
660
+ }
661
+
662
+ /** Projects the current admitted RunGraph, including autonomous nodes, into the canonical Eager executor plan. */
663
+ export function projectAIDataWorkflowRunGraphPlan(graph: AIDataWorkflowRunGraph): EagerDataFlowAuthoringPlan {
664
+ const activeOrder = graph.declarationOrder.filter((nodeId) => graph.nodes[nodeId]?.status !== 'Removed');
665
+ const nodePlans = activeOrder.map((nodeId): EagerDataFlowNodePlan => {
666
+ const node = graph.nodes[nodeId];
667
+ const base = graph.binding.definition.nodeById[nodeId]
668
+ ?? graph.binding.definition.nodes.find((candidate) => candidate.id === nodeId);
669
+ const dataDependencies = new Set(Object.values(node.inputs).flatMap((binding) => (
670
+ isRunGraphPortReference(binding) ? [binding.nodeId] : []
671
+ )));
672
+ const waitFor = node.dependsOn
673
+ .filter((dependency) => !dataDependencies.has(dependency))
674
+ .map((dependency) => ({ ref: `flow-node://#${dependency}`, nodeId: dependency }));
675
+ const common = { id: node.id, declarationIndex: node.declarationIndex, waitFor };
676
+ if (node.tag === 'EntryNode') return { ...common, tag: 'EntryNode', outputs: [...node.outputs] };
677
+ if (node.tag === 'ReturnNode') {
678
+ return { ...common, tag: 'ReturnNode', inputs: { ...node.inputs } } as EagerDataFlowNodePlan;
679
+ }
680
+ if (node.tag === 'SubFlowNode') {
681
+ if (!node.subflow) throw new AIDataWorkflowRunGraphError(`SubFlowNode ${node.id} has no durable child binding.`);
682
+ return {
683
+ ...common,
684
+ tag: 'SubFlowNode',
685
+ flow: node.subflow.flowRef,
686
+ inputs: { ...node.inputs },
687
+ outputs: [...node.outputs],
688
+ } as EagerDataFlowNodePlan;
689
+ }
690
+ if (node.tag !== 'TransformNode' && node.tag !== 'SinkNode') {
691
+ throw new AIDataWorkflowRunGraphError(`RunGraph node ${node.id} has unsupported executable tag ${node.tag}.`);
692
+ }
693
+ const configuredSource = typeof node.config.src === 'string' ? node.config.src : undefined;
694
+ const configuredImpl = typeof node.config.impl === 'string' ? node.config.impl : undefined;
695
+ const baseExecutable = base && (base.tag === 'TransformNode' || base.tag === 'SinkNode') ? base : undefined;
696
+ const executable = {
697
+ ...(configuredSource ?? baseExecutable?.src ? { src: configuredSource ?? baseExecutable?.src } : {}),
698
+ ...(configuredImpl ?? baseExecutable?.impl ? { impl: configuredImpl ?? baseExecutable?.impl } : {}),
699
+ };
700
+ return {
701
+ ...common,
702
+ tag: node.tag,
703
+ inputs: { ...node.inputs },
704
+ ...(node.tag === 'TransformNode' ? { outputs: [...node.outputs] } : {}),
705
+ ...executable,
706
+ config: { ...node.config },
707
+ } as EagerDataFlowNodePlan;
708
+ });
709
+ const nodeById = Object.fromEntries(nodePlans.map((node) => [node.id, node]));
710
+ const dataEdges = nodePlans.flatMap((node) => node.tag === 'EntryNode' ? [] : Object.entries(node.inputs).flatMap(
711
+ ([input, binding]) => isRunGraphPortReference(binding) ? [{
712
+ kind: 'data' as const,
713
+ fromNodeId: binding.nodeId,
714
+ fromPort: binding.port,
715
+ toNodeId: node.id,
716
+ toInput: input,
717
+ ref: binding.ref,
718
+ }] : [],
719
+ ));
720
+ const waitForEdges = nodePlans.flatMap((node) => node.waitFor.map((reference) => ({
721
+ kind: 'waitFor' as const,
722
+ fromNodeId: reference.nodeId,
723
+ toNodeId: node.id,
724
+ ref: reference.ref,
725
+ })));
726
+ const subflowEdges = nodePlans.flatMap((node) => node.tag === 'SubFlowNode' ? [{
727
+ kind: 'subflow' as const,
728
+ fromFlowId: graph.binding.definition.fqn,
729
+ nodeId: node.id,
730
+ targetFlowId: node.flow.slice('eager-data-flow://'.length),
731
+ ref: node.flow,
732
+ }] : []);
733
+ return {
734
+ ...graph.binding.definition,
735
+ nodes: nodePlans,
736
+ nodeById,
737
+ declarationOrder: activeOrder,
738
+ dataEdges,
739
+ waitForEdges,
740
+ subflowEdges,
741
+ diagnostics: [],
742
+ };
743
+ }
744
+
576
745
  function makePatchNode(
577
746
  spec: AIDataWorkflowGraphPatchNodeSpec,
578
747
  generation: number,
@@ -587,6 +756,8 @@ function makePatchNode(
587
756
  platformDefaultPolicy,
588
757
  });
589
758
  assertReusePolicyAuthority(reusePolicy, nodeType, config, platformDefaultPolicy, spec.id);
759
+ const subflow = spec.subflow ? cloneSubflow(spec.subflow) : undefined;
760
+ assertSubflowBinding(tag, subflow, spec.id);
590
761
  return {
591
762
  id: spec.id,
592
763
  tag,
@@ -597,6 +768,7 @@ function makePatchNode(
597
768
  inputs: cloneRecord(spec.inputs),
598
769
  outputs: [...(spec.outputs ?? [])],
599
770
  config,
771
+ ...(subflow ? { subflow } : {}),
600
772
  dependsOn: uniqueStrings(spec.dependsOn ?? []),
601
773
  dependents: [],
602
774
  reusePolicy,
@@ -662,6 +834,9 @@ function applyInvalidations(
662
834
  const {
663
835
  result: _staleResult,
664
836
  semanticFingerprint: _staleFingerprint,
837
+ childInvocation: _staleChildInvocation,
838
+ childFreezeReceipt: _staleChildFreezeReceipt,
839
+ childTerminalReceipt: _staleChildTerminalReceipt,
665
840
  ...currentFacts
666
841
  } = node;
667
842
  nextNodes = {
@@ -782,6 +957,13 @@ export function applyAIDataWorkflowGraphPatch(
782
957
  );
783
958
  }
784
959
  const config = operation.changes.config ? cloneRecord(operation.changes.config) : existing.config;
960
+ const tag = operation.changes.tag ?? existing.tag;
961
+ const subflow = Object.prototype.hasOwnProperty.call(operation.changes, 'subflow')
962
+ ? operation.changes.subflow === null || operation.changes.subflow === undefined
963
+ ? undefined
964
+ : cloneSubflow(operation.changes.subflow)
965
+ : existing.subflow;
966
+ assertSubflowBinding(tag, subflow, existing.id);
785
967
  const nodeType = operation.changes.nodeType ?? existing.nodeType;
786
968
  const dependsOn = operation.changes.dependsOn
787
969
  ? uniqueStrings(operation.changes.dependsOn)
@@ -797,17 +979,26 @@ export function applyAIDataWorkflowGraphPatch(
797
979
  : existing.reusePolicy
798
980
  );
799
981
  assertReusePolicyAuthority(reusePolicy, nodeType, config, graph.platformDefaultPolicy, existing.id);
982
+ const { subflow: _existingSubflow, ...existingWithoutSubflow } = existing;
983
+ const {
984
+ childInvocation: _staleChildInvocation,
985
+ childFreezeReceipt: _staleChildFreezeReceipt,
986
+ childTerminalReceipt: _staleChildTerminalReceipt,
987
+ ...existingWithoutChild
988
+ } = existingWithoutSubflow;
989
+ const { subflow: _changedSubflow, ...changesWithoutSubflow } = operation.changes;
800
990
  nodes = {
801
991
  ...nodes,
802
992
  [existing.id]: {
803
- ...existing,
804
- ...operation.changes,
805
- tag: operation.changes.tag ?? existing.tag,
993
+ ...existingWithoutChild,
994
+ ...changesWithoutSubflow,
995
+ tag,
806
996
  nodeType,
807
997
  generation,
808
998
  inputs: operation.changes.inputs ? cloneRecord(operation.changes.inputs) : existing.inputs,
809
999
  outputs: operation.changes.outputs ? [...operation.changes.outputs] : existing.outputs,
810
1000
  config,
1001
+ ...(subflow ? { subflow } : {}),
811
1002
  dependsOn,
812
1003
  reusePolicy,
813
1004
  },
@@ -833,6 +1024,9 @@ export function applyAIDataWorkflowGraphPatch(
833
1024
  result: _staleResult,
834
1025
  semanticFingerprint: _staleFingerprint,
835
1026
  invalidatedBy: _activeInvalidation,
1027
+ childInvocation: _staleChildInvocation,
1028
+ childFreezeReceipt: _staleChildFreezeReceipt,
1029
+ childTerminalReceipt: _staleChildTerminalReceipt,
836
1030
  ...currentFacts
837
1031
  } = existing;
838
1032
  nodes = {