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/package.json +6 -6
- package/src/autonomous-control.ts +1466 -0
- package/src/child-invocation.ts +376 -0
- package/src/filesystem.ts +203 -30
- package/src/index.ts +198 -4
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
...
|
|
804
|
-
...
|
|
805
|
-
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 = {
|