ai-data-workflow-logic 0.1.12 → 0.2.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/package.json +7 -7
- package/src/autonomous-control.ts +104 -13
- package/src/child-invocation.ts +376 -0
- package/src/filesystem.ts +203 -30
- package/src/index.ts +197 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-data-workflow-logic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "EagerDataFlow-backed AI Data Workflow RunGraph helpers",
|
|
6
6
|
"type": "module",
|
|
@@ -11,15 +11,15 @@
|
|
|
11
11
|
"./filesystem": "./src/filesystem.ts"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"ai-data-workflow-contract": "0.
|
|
15
|
-
"ai-workflow-contract": "0.
|
|
16
|
-
"ai-workflow-logic": "0.
|
|
17
|
-
"eager-data-flow-contract": "0.1.
|
|
18
|
-
"eager-data-flow-logic": "0.1.
|
|
14
|
+
"ai-data-workflow-contract": "0.2.0",
|
|
15
|
+
"ai-workflow-contract": "0.2.0",
|
|
16
|
+
"ai-workflow-logic": "0.2.0",
|
|
17
|
+
"eager-data-flow-contract": "0.1.4",
|
|
18
|
+
"eager-data-flow-logic": "0.1.7",
|
|
19
19
|
"flow-step-space-logic": "0.1.1",
|
|
20
20
|
"task-manager-contract": "0.1.3",
|
|
21
21
|
"task-manager-logic": "0.1.6",
|
|
22
|
-
"work-ctrl-flow-logic": "0.1.
|
|
22
|
+
"work-ctrl-flow-logic": "0.1.5"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"src"
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type AIDataWorkflowNodeInputs,
|
|
22
22
|
type AIDataWorkflowRunGraph,
|
|
23
23
|
type AIDataWorkflowRunNode,
|
|
24
|
+
type AIDataWorkflowSubFlowBinding,
|
|
24
25
|
} from 'ai-data-workflow-contract';
|
|
25
26
|
import type { EagerDataFlowValue } from 'eager-data-flow-contract';
|
|
26
27
|
|
|
@@ -239,13 +240,20 @@ function assertResourceRef(value: unknown, location: string): void {
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
function assertEagerDataFlowRef(value: unknown, location: string): void {
|
|
244
|
+
assertNonEmptyString(value, location);
|
|
245
|
+
if (!value.startsWith('eager-data-flow://') || value.length === 'eager-data-flow://'.length) {
|
|
246
|
+
fail('AI_DATA_CONTROL_FLOW_REF', `${location} must be an exact eager-data-flow:// reference.`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
242
250
|
function assertCapability(capability: AIDataControlCapability, key: string): void {
|
|
243
251
|
assertNonEmptyString(capability.capabilityId, `capabilities.${key}.capabilityId`);
|
|
244
252
|
if (capability.capabilityId !== key) {
|
|
245
253
|
fail('AI_DATA_CONTROL_CAPABILITY_ID', `Capability key ${key} must equal capabilityId ${capability.capabilityId}.`);
|
|
246
254
|
}
|
|
247
|
-
if (capability.tag !== 'TransformNode' && capability.tag !== 'SinkNode') {
|
|
248
|
-
fail('AI_DATA_CONTROL_CAPABILITY_TAG', `Capability ${key} must use TransformNode or
|
|
255
|
+
if (capability.tag !== 'TransformNode' && capability.tag !== 'SinkNode' && capability.tag !== 'SubFlowNode') {
|
|
256
|
+
fail('AI_DATA_CONTROL_CAPABILITY_TAG', `Capability ${key} must use TransformNode, SinkNode or SubFlowNode.`);
|
|
249
257
|
}
|
|
250
258
|
assertNonEmptyString(capability.nodeType, `capabilities.${key}.nodeType`);
|
|
251
259
|
assertSchemaMap(capability.inputSchemaRefs, `capabilities.${key}.inputSchemaRefs`);
|
|
@@ -260,10 +268,36 @@ function assertCapability(capability: AIDataControlCapability, key: string): voi
|
|
|
260
268
|
fail('AI_DATA_CONTROL_CAPABILITY_CONFIG', `Capability ${key} fixedConfig uses reserved control keys.`);
|
|
261
269
|
}
|
|
262
270
|
if (capability.implementation.kind === 'code') {
|
|
271
|
+
if (capability.tag === 'SubFlowNode') {
|
|
272
|
+
fail('AI_DATA_CONTROL_CAPABILITY_IMPLEMENTATION', `SubFlow capability ${key} must use a subflow implementation.`);
|
|
273
|
+
}
|
|
263
274
|
assertResourceRef(capability.implementation.codeRef, `capabilities.${key}.implementation.codeRef`);
|
|
264
275
|
} else if (capability.implementation.kind === 'agent') {
|
|
276
|
+
if (capability.tag === 'SubFlowNode') {
|
|
277
|
+
fail('AI_DATA_CONTROL_CAPABILITY_IMPLEMENTATION', `SubFlow capability ${key} must use a subflow implementation.`);
|
|
278
|
+
}
|
|
265
279
|
assertResourceRef(capability.implementation.agentDefinitionRef, `capabilities.${key}.implementation.agentDefinitionRef`);
|
|
266
280
|
assertResourceRef(capability.implementation.taskProofRef, `capabilities.${key}.implementation.taskProofRef`);
|
|
281
|
+
} else if (capability.implementation.kind === 'subflow') {
|
|
282
|
+
if (capability.tag !== 'SubFlowNode' || capability.nodeType !== 'subflow') {
|
|
283
|
+
fail('AI_DATA_CONTROL_CAPABILITY_IMPLEMENTATION', `Subflow implementation ${key} requires SubFlowNode/subflow.`);
|
|
284
|
+
}
|
|
285
|
+
assertResourceRef(capability.implementation.definitionRef, `capabilities.${key}.implementation.definitionRef`);
|
|
286
|
+
assertEagerDataFlowRef(capability.implementation.flowRef, `capabilities.${key}.implementation.flowRef`);
|
|
287
|
+
assertNonEmptyString(capability.implementation.contractDigest, `capabilities.${key}.implementation.contractDigest`);
|
|
288
|
+
assertNonEmptyString(capability.implementation.callGraphDigest, `capabilities.${key}.implementation.callGraphDigest`);
|
|
289
|
+
if (!Array.isArray(capability.implementation.transitiveChildFlowRefs)) {
|
|
290
|
+
fail('AI_DATA_CONTROL_CALL_GRAPH', `capabilities.${key}.implementation.transitiveChildFlowRefs must be an array.`);
|
|
291
|
+
}
|
|
292
|
+
const childRefs = capability.implementation.transitiveChildFlowRefs;
|
|
293
|
+
childRefs.forEach((ref, index) => assertEagerDataFlowRef(
|
|
294
|
+
ref,
|
|
295
|
+
`capabilities.${key}.implementation.transitiveChildFlowRefs[${index}]`,
|
|
296
|
+
));
|
|
297
|
+
if (new Set(childRefs).size !== childRefs.length
|
|
298
|
+
|| childRefs.some((ref, index) => index > 0 && compareCodeUnits(childRefs[index - 1]!, ref) >= 0)) {
|
|
299
|
+
fail('AI_DATA_CONTROL_CALL_GRAPH', `capabilities.${key}.implementation.transitiveChildFlowRefs must be unique and sorted.`);
|
|
300
|
+
}
|
|
267
301
|
} else {
|
|
268
302
|
fail('AI_DATA_CONTROL_CAPABILITY_IMPLEMENTATION', `Capability ${key} has an unknown implementation kind.`);
|
|
269
303
|
}
|
|
@@ -516,6 +550,17 @@ function normalizeRevisionOperation(
|
|
|
516
550
|
...(dependsOn === undefined ? {} : { dependsOn }),
|
|
517
551
|
};
|
|
518
552
|
}
|
|
553
|
+
if (value.op === 'add-subflow' || value.op === 'rewire-subflow') {
|
|
554
|
+
const record = exactRecord(value, ['op', 'nodeId', 'subflowCapabilityId', 'inputs'], ['dependsOn'], location);
|
|
555
|
+
const dependsOn = normalizeDependsOn(record.dependsOn, `${location}.dependsOn`);
|
|
556
|
+
return {
|
|
557
|
+
op: value.op,
|
|
558
|
+
nodeId: stringField(record, 'nodeId', location),
|
|
559
|
+
subflowCapabilityId: stringField(record, 'subflowCapabilityId', location),
|
|
560
|
+
inputs: normalizeInputs(runtime, record.inputs, `${location}.inputs`),
|
|
561
|
+
...(dependsOn === undefined ? {} : { dependsOn }),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
519
564
|
fail('AI_DATA_CONTROL_DECISION_SHAPE', `${location}.op is unsupported.`);
|
|
520
565
|
}
|
|
521
566
|
|
|
@@ -836,7 +881,7 @@ function nodeContracts(
|
|
|
836
881
|
|
|
837
882
|
function validateInputs(
|
|
838
883
|
runtime: AIDataControlRuntime,
|
|
839
|
-
operation:
|
|
884
|
+
operation: Exclude<AIDataControlRevisionOperation, { op: 'remove-node' }>,
|
|
840
885
|
capability: AIDataControlCapability,
|
|
841
886
|
contracts: ReadonlyMap<string, MutableNodeContract>,
|
|
842
887
|
operationIndex: number,
|
|
@@ -969,6 +1014,7 @@ function capabilityConfig(
|
|
|
969
1014
|
if (capability.implementation.kind === 'code') {
|
|
970
1015
|
return { ...base, src: capability.implementation.codeRef };
|
|
971
1016
|
}
|
|
1017
|
+
if (capability.implementation.kind === 'subflow') return base;
|
|
972
1018
|
return {
|
|
973
1019
|
...base,
|
|
974
1020
|
agent: {
|
|
@@ -978,8 +1024,24 @@ function capabilityConfig(
|
|
|
978
1024
|
};
|
|
979
1025
|
}
|
|
980
1026
|
|
|
1027
|
+
function capabilitySubflowBinding(
|
|
1028
|
+
capability: AIDataControlCapability,
|
|
1029
|
+
catalog: AIDataControlCapabilityCatalog,
|
|
1030
|
+
): AIDataWorkflowSubFlowBinding | undefined {
|
|
1031
|
+
if (capability.implementation.kind !== 'subflow') return undefined;
|
|
1032
|
+
return {
|
|
1033
|
+
kind: 'autonomous',
|
|
1034
|
+
schemaVersion: 'depa.ai-data-subflow-binding/v1',
|
|
1035
|
+
capabilityId: capability.capabilityId,
|
|
1036
|
+
catalogDigest: catalog.digest,
|
|
1037
|
+
definitionRef: capability.implementation.definitionRef,
|
|
1038
|
+
flowRef: capability.implementation.flowRef,
|
|
1039
|
+
contractDigest: capability.implementation.contractDigest,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
981
1043
|
function operationDependencies(
|
|
982
|
-
operation:
|
|
1044
|
+
operation: Exclude<AIDataControlRevisionOperation, { op: 'remove-node' }>,
|
|
983
1045
|
): readonly string[] {
|
|
984
1046
|
return [...new Set([
|
|
985
1047
|
...(operation.dependsOn ?? []),
|
|
@@ -998,6 +1060,7 @@ function sameNodeRevision(
|
|
|
998
1060
|
inputs: node.inputs,
|
|
999
1061
|
outputs: node.outputs,
|
|
1000
1062
|
config: node.config,
|
|
1063
|
+
subflow: node.subflow ?? null,
|
|
1001
1064
|
dependsOn: node.dependsOn,
|
|
1002
1065
|
}) === runtime.stableDigest({
|
|
1003
1066
|
tag: operation.changes.tag ?? node.tag,
|
|
@@ -1005,6 +1068,9 @@ function sameNodeRevision(
|
|
|
1005
1068
|
inputs: operation.changes.inputs ?? node.inputs,
|
|
1006
1069
|
outputs: operation.changes.outputs ?? node.outputs,
|
|
1007
1070
|
config: operation.changes.config ?? node.config,
|
|
1071
|
+
subflow: (Object.prototype.hasOwnProperty.call(operation.changes, 'subflow')
|
|
1072
|
+
? operation.changes.subflow
|
|
1073
|
+
: node.subflow) ?? null,
|
|
1008
1074
|
dependsOn: operation.changes.dependsOn ?? node.dependsOn,
|
|
1009
1075
|
});
|
|
1010
1076
|
}
|
|
@@ -1160,7 +1226,7 @@ function admitNormalizedAIDataControlDecision(
|
|
|
1160
1226
|
|
|
1161
1227
|
const targets = new Set<string>();
|
|
1162
1228
|
for (let index = 0; index < input.decision.operations.length; index += 1) {
|
|
1163
|
-
const operation = input.decision.operations[index]!;
|
|
1229
|
+
const operation: AIDataControlRevisionOperation = input.decision.operations[index]!;
|
|
1164
1230
|
if (targets.has(operation.nodeId)) {
|
|
1165
1231
|
return rejected(runtime, input.decision, [
|
|
1166
1232
|
feedback('DUPLICATE_TARGET', `Revision targets ${operation.nodeId} more than once.`, {
|
|
@@ -1181,7 +1247,7 @@ function admitNormalizedAIDataControlDecision(
|
|
|
1181
1247
|
const removed = new Set<string>();
|
|
1182
1248
|
|
|
1183
1249
|
for (let index = 0; index < input.decision.operations.length; index += 1) {
|
|
1184
|
-
const operation = input.decision.operations[index]!;
|
|
1250
|
+
const operation: AIDataControlRevisionOperation = input.decision.operations[index]!;
|
|
1185
1251
|
const existingContract = contracts.get(operation.nodeId);
|
|
1186
1252
|
if (operation.op === 'remove-node') {
|
|
1187
1253
|
if (!existingContract || existingContract.removed) {
|
|
@@ -1202,13 +1268,15 @@ function admitNormalizedAIDataControlDecision(
|
|
|
1202
1268
|
continue;
|
|
1203
1269
|
}
|
|
1204
1270
|
|
|
1205
|
-
|
|
1271
|
+
const isAdd = operation.op === 'add-capability' || operation.op === 'add-subflow';
|
|
1272
|
+
const isRewire = operation.op === 'rewire-capability' || operation.op === 'rewire-subflow';
|
|
1273
|
+
if (isAdd && existingContract) {
|
|
1206
1274
|
return rejected(runtime, input.decision, [feedback('INVALID_DECISION', `Node ${operation.nodeId} already exists.`, {
|
|
1207
1275
|
operationIndex: index,
|
|
1208
1276
|
nodeId: operation.nodeId,
|
|
1209
1277
|
})]);
|
|
1210
1278
|
}
|
|
1211
|
-
if (
|
|
1279
|
+
if (isRewire) {
|
|
1212
1280
|
if (!existingContract || existingContract.removed) {
|
|
1213
1281
|
return rejected(runtime, input.decision, [feedback('UNKNOWN_NODE', `Node ${operation.nodeId} is not active.`, {
|
|
1214
1282
|
operationIndex: index,
|
|
@@ -1223,15 +1291,37 @@ function admitNormalizedAIDataControlDecision(
|
|
|
1223
1291
|
}
|
|
1224
1292
|
}
|
|
1225
1293
|
|
|
1226
|
-
const
|
|
1294
|
+
const capabilityId = operation.op === 'add-subflow' || operation.op === 'rewire-subflow'
|
|
1295
|
+
? operation.subflowCapabilityId
|
|
1296
|
+
: operation.capabilityId;
|
|
1297
|
+
const capability = input.catalog.capabilities[capabilityId];
|
|
1227
1298
|
if (!capability) {
|
|
1228
|
-
return rejected(runtime, input.decision, [feedback('UNKNOWN_CAPABILITY', `Capability ${
|
|
1299
|
+
return rejected(runtime, input.decision, [feedback('UNKNOWN_CAPABILITY', `Capability ${capabilityId} is not frozen.`, {
|
|
1229
1300
|
operationIndex: index,
|
|
1230
1301
|
nodeId: operation.nodeId,
|
|
1231
1302
|
})]);
|
|
1232
1303
|
}
|
|
1304
|
+
const expectsSubflow = operation.op === 'add-subflow' || operation.op === 'rewire-subflow';
|
|
1305
|
+
if (expectsSubflow !== (capability.implementation.kind === 'subflow')) {
|
|
1306
|
+
return rejected(runtime, input.decision, [feedback(
|
|
1307
|
+
'INVALID_DECISION',
|
|
1308
|
+
`Operation ${operation.op} does not match frozen capability ${capabilityId}.`,
|
|
1309
|
+
{ operationIndex: index, nodeId: operation.nodeId },
|
|
1310
|
+
)]);
|
|
1311
|
+
}
|
|
1233
1312
|
const inputProblem = validateInputs(runtime, operation, capability, contracts, index);
|
|
1234
1313
|
if (inputProblem) return rejected(runtime, input.decision, [inputProblem]);
|
|
1314
|
+
if (capability.implementation.kind === 'subflow') {
|
|
1315
|
+
const parentFlowRef = `eager-data-flow://${input.graph.binding.definition.fqn}` as `eager-data-flow://${string}`;
|
|
1316
|
+
if (capability.implementation.flowRef === parentFlowRef
|
|
1317
|
+
|| capability.implementation.transitiveChildFlowRefs.includes(parentFlowRef)) {
|
|
1318
|
+
return rejected(runtime, input.decision, [feedback(
|
|
1319
|
+
'RECURSIVE_SUBFLOW',
|
|
1320
|
+
`Subflow capability ${capability.capabilityId} would recurse into ${parentFlowRef}.`,
|
|
1321
|
+
{ operationIndex: index, nodeId: operation.nodeId },
|
|
1322
|
+
)]);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1235
1325
|
const dependsOn = operationDependencies(operation);
|
|
1236
1326
|
for (const dependency of dependsOn) {
|
|
1237
1327
|
const dependencyContract = contracts.get(dependency);
|
|
@@ -1242,6 +1332,7 @@ function admitNormalizedAIDataControlDecision(
|
|
|
1242
1332
|
})]);
|
|
1243
1333
|
}
|
|
1244
1334
|
}
|
|
1335
|
+
const subflow = capabilitySubflowBinding(capability, input.catalog);
|
|
1245
1336
|
const nodeFacts = {
|
|
1246
1337
|
tag: capability.tag,
|
|
1247
1338
|
nodeType: capability.nodeType,
|
|
@@ -1250,16 +1341,16 @@ function admitNormalizedAIDataControlDecision(
|
|
|
1250
1341
|
config: capabilityConfig(capability, input.catalog),
|
|
1251
1342
|
dependsOn,
|
|
1252
1343
|
};
|
|
1253
|
-
if (
|
|
1344
|
+
if (isAdd) {
|
|
1254
1345
|
patchOperations.push({
|
|
1255
1346
|
op: 'add-node',
|
|
1256
|
-
node: { id: operation.nodeId, ...nodeFacts },
|
|
1347
|
+
node: { id: operation.nodeId, ...nodeFacts, ...(subflow ? { subflow } : {}) },
|
|
1257
1348
|
});
|
|
1258
1349
|
} else {
|
|
1259
1350
|
patchOperations.push({
|
|
1260
1351
|
op: 'update-node',
|
|
1261
1352
|
nodeId: operation.nodeId,
|
|
1262
|
-
changes: nodeFacts,
|
|
1353
|
+
changes: { ...nodeFacts, subflow: subflow ?? null },
|
|
1263
1354
|
});
|
|
1264
1355
|
}
|
|
1265
1356
|
contracts.set(operation.nodeId, {
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AIDataWorkflowAutonomousSubFlowBinding,
|
|
3
|
+
AIDataWorkflowChildFreezeReceipt,
|
|
4
|
+
AIDataWorkflowChildInvocationIdentity,
|
|
5
|
+
AIDataWorkflowChildInvocationObservation,
|
|
6
|
+
AIDataWorkflowChildTerminalReceipt,
|
|
7
|
+
AIDataWorkflowRunGraph,
|
|
8
|
+
} from 'ai-data-workflow-contract';
|
|
9
|
+
import { AI_DATA_CHILD_INVOCATION_SCHEMA_VERSION } from 'ai-data-workflow-contract';
|
|
10
|
+
import { normalizeFlowClosedObject } from 'ai-workflow-logic';
|
|
11
|
+
|
|
12
|
+
export class AIDataWorkflowChildInvocationError extends Error {
|
|
13
|
+
constructor(message: string) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'AIDataWorkflowChildInvocationError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fnv1a64(value: string): string {
|
|
20
|
+
let hash = 0xcbf29ce484222325n;
|
|
21
|
+
const prime = 0x100000001b3n;
|
|
22
|
+
const mask = 0xffffffffffffffffn;
|
|
23
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
24
|
+
hash ^= BigInt(value.charCodeAt(index));
|
|
25
|
+
hash = (hash * prime) & mask;
|
|
26
|
+
}
|
|
27
|
+
return hash.toString(16).padStart(16, '0');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const compareCodeUnits = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0;
|
|
31
|
+
|
|
32
|
+
function record(value: unknown, keys: readonly string[], location: string): Readonly<Record<string, unknown>> {
|
|
33
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
34
|
+
throw new AIDataWorkflowChildInvocationError(`${location} must be an object.`);
|
|
35
|
+
}
|
|
36
|
+
const input = value as Readonly<Record<string, unknown>>;
|
|
37
|
+
const actual = Object.keys(input).sort(compareCodeUnits);
|
|
38
|
+
const expected = [...keys].sort(compareCodeUnits);
|
|
39
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
40
|
+
throw new AIDataWorkflowChildInvocationError(`${location} fields are invalid.`);
|
|
41
|
+
}
|
|
42
|
+
return input;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function nonEmpty(value: unknown, location: string): string {
|
|
46
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
47
|
+
throw new AIDataWorkflowChildInvocationError(`${location} must be a non-empty string.`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function prefixed(value: unknown, prefix: string, location: string): string {
|
|
53
|
+
const normalized = nonEmpty(value, location);
|
|
54
|
+
if (!normalized.startsWith(prefix) || normalized.length === prefix.length) {
|
|
55
|
+
throw new AIDataWorkflowChildInvocationError(`${location} must start with ${prefix}.`);
|
|
56
|
+
}
|
|
57
|
+
return normalized;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function schema(value: unknown, location: string): void {
|
|
61
|
+
if (value !== AI_DATA_CHILD_INVOCATION_SCHEMA_VERSION) {
|
|
62
|
+
throw new AIDataWorkflowChildInvocationError(`${location} uses an unsupported schema version.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeSubflow(value: unknown, location: string): AIDataWorkflowAutonomousSubFlowBinding {
|
|
67
|
+
const input = record(value, [
|
|
68
|
+
'kind', 'schemaVersion', 'capabilityId', 'catalogDigest', 'definitionRef', 'flowRef', 'contractDigest',
|
|
69
|
+
], location);
|
|
70
|
+
if (input.kind !== 'autonomous' || input.schemaVersion !== 'depa.ai-data-subflow-binding/v1') {
|
|
71
|
+
throw new AIDataWorkflowChildInvocationError(`${location} must be an autonomous subflow binding.`);
|
|
72
|
+
}
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
kind: 'autonomous',
|
|
75
|
+
schemaVersion: 'depa.ai-data-subflow-binding/v1',
|
|
76
|
+
capabilityId: nonEmpty(input.capabilityId, `${location}.capabilityId`),
|
|
77
|
+
catalogDigest: nonEmpty(input.catalogDigest, `${location}.catalogDigest`),
|
|
78
|
+
definitionRef: prefixed(input.definitionRef, 'resource://', `${location}.definitionRef`) as `resource://${string}`,
|
|
79
|
+
flowRef: prefixed(input.flowRef, 'eager-data-flow://', `${location}.flowRef`) as `eager-data-flow://${string}`,
|
|
80
|
+
contractDigest: nonEmpty(input.contractDigest, `${location}.contractDigest`),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sameSubflow(
|
|
85
|
+
left: AIDataWorkflowAutonomousSubFlowBinding,
|
|
86
|
+
right: AIDataWorkflowAutonomousSubFlowBinding,
|
|
87
|
+
): boolean {
|
|
88
|
+
return left.kind === right.kind
|
|
89
|
+
&& left.schemaVersion === right.schemaVersion
|
|
90
|
+
&& left.capabilityId === right.capabilityId
|
|
91
|
+
&& left.catalogDigest === right.catalogDigest
|
|
92
|
+
&& left.definitionRef === right.definitionRef
|
|
93
|
+
&& left.flowRef === right.flowRef
|
|
94
|
+
&& left.contractDigest === right.contractDigest;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createAIDataWorkflowChildInvocationIdentity(input: {
|
|
98
|
+
readonly graph: AIDataWorkflowRunGraph;
|
|
99
|
+
readonly parentInstanceId: string;
|
|
100
|
+
readonly nodeId: string;
|
|
101
|
+
}): AIDataWorkflowChildInvocationIdentity {
|
|
102
|
+
const node = input.graph.nodes[input.nodeId];
|
|
103
|
+
if (!node || node.tag !== 'SubFlowNode' || node.subflow?.kind !== 'autonomous') {
|
|
104
|
+
throw new AIDataWorkflowChildInvocationError(
|
|
105
|
+
`AI Data Workflow node ${input.nodeId} is not an autonomous SubFlowNode.`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const canonical = JSON.stringify([
|
|
109
|
+
input.parentInstanceId,
|
|
110
|
+
input.graph.runId,
|
|
111
|
+
node.id,
|
|
112
|
+
node.generation,
|
|
113
|
+
node.subflow.capabilityId,
|
|
114
|
+
node.subflow.catalogDigest,
|
|
115
|
+
node.subflow.definitionRef,
|
|
116
|
+
node.subflow.flowRef,
|
|
117
|
+
node.subflow.contractDigest,
|
|
118
|
+
]);
|
|
119
|
+
const suffix = fnv1a64(canonical);
|
|
120
|
+
return normalizeAIDataWorkflowChildInvocationIdentity({
|
|
121
|
+
schemaVersion: AI_DATA_CHILD_INVOCATION_SCHEMA_VERSION,
|
|
122
|
+
invocationKey: `ai-data-child:${suffix}`,
|
|
123
|
+
parentInstanceId: input.parentInstanceId,
|
|
124
|
+
parentRunId: input.graph.runId,
|
|
125
|
+
parentNodeId: node.id,
|
|
126
|
+
parentGeneration: node.generation,
|
|
127
|
+
childInstanceId: `${input.parentInstanceId}--child-${suffix}`,
|
|
128
|
+
childRunId: `${input.graph.runId}--child-${suffix}`,
|
|
129
|
+
subflow: node.subflow,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Pure parent-side reservation. A differing winner is rejected; an exact retry is idempotent. */
|
|
134
|
+
export function reserveAIDataWorkflowChildInvocation(input: {
|
|
135
|
+
readonly graph: AIDataWorkflowRunGraph;
|
|
136
|
+
readonly identity: AIDataWorkflowChildInvocationIdentity;
|
|
137
|
+
}): AIDataWorkflowRunGraph {
|
|
138
|
+
const identity = normalizeAIDataWorkflowChildInvocationIdentity(input.identity);
|
|
139
|
+
const node = input.graph.nodes[identity.parentNodeId];
|
|
140
|
+
if (!node || node.tag !== 'SubFlowNode' || node.subflow?.kind !== 'autonomous') {
|
|
141
|
+
throw new AIDataWorkflowChildInvocationError(
|
|
142
|
+
`AI Data Workflow node ${identity.parentNodeId} is not an autonomous SubFlowNode.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (input.graph.runId !== identity.parentRunId || node.generation !== identity.parentGeneration
|
|
146
|
+
|| !sameSubflow(node.subflow, identity.subflow)) {
|
|
147
|
+
throw new AIDataWorkflowChildInvocationError('Child invocation reservation does not match the current parent generation.');
|
|
148
|
+
}
|
|
149
|
+
if (node.status === 'Succeeded' || node.status === 'Failed' || node.status === 'Reused' || node.status === 'Removed') {
|
|
150
|
+
throw new AIDataWorkflowChildInvocationError(`Cannot reserve a child for terminal node ${node.id}.`);
|
|
151
|
+
}
|
|
152
|
+
if (node.childInvocation) {
|
|
153
|
+
const current = normalizeAIDataWorkflowChildInvocationIdentity(node.childInvocation);
|
|
154
|
+
if (JSON.stringify(current) !== JSON.stringify(identity)) {
|
|
155
|
+
throw new AIDataWorkflowChildInvocationError(`Node ${node.id} already reserved another child invocation.`);
|
|
156
|
+
}
|
|
157
|
+
return input.graph;
|
|
158
|
+
}
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
...input.graph,
|
|
161
|
+
nodes: Object.freeze({
|
|
162
|
+
...input.graph.nodes,
|
|
163
|
+
[node.id]: Object.freeze({
|
|
164
|
+
...node,
|
|
165
|
+
status: 'Running' as const,
|
|
166
|
+
childInvocation: identity,
|
|
167
|
+
}),
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function recordAIDataWorkflowChildFreezeReceipt(input: {
|
|
173
|
+
readonly graph: AIDataWorkflowRunGraph;
|
|
174
|
+
readonly nodeId: string;
|
|
175
|
+
readonly receipt: AIDataWorkflowChildFreezeReceipt;
|
|
176
|
+
}): AIDataWorkflowRunGraph {
|
|
177
|
+
const node = input.graph.nodes[input.nodeId];
|
|
178
|
+
if (!node?.childInvocation) {
|
|
179
|
+
throw new AIDataWorkflowChildInvocationError(`Node ${input.nodeId} has no reserved child invocation.`);
|
|
180
|
+
}
|
|
181
|
+
const receipt = normalizeAIDataWorkflowChildFreezeReceipt(input.receipt, node.childInvocation);
|
|
182
|
+
if (node.childFreezeReceipt) {
|
|
183
|
+
if (JSON.stringify(node.childFreezeReceipt) === JSON.stringify(receipt)) return input.graph;
|
|
184
|
+
throw new AIDataWorkflowChildInvocationError(`Node ${input.nodeId} already froze another child definition.`);
|
|
185
|
+
}
|
|
186
|
+
return Object.freeze({
|
|
187
|
+
...input.graph,
|
|
188
|
+
nodes: Object.freeze({
|
|
189
|
+
...input.graph.nodes,
|
|
190
|
+
[node.id]: Object.freeze({ ...node, childFreezeReceipt: receipt }),
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Accepts an immutable child terminal receipt as the exact parent node settlement. */
|
|
196
|
+
export function acceptAIDataWorkflowChildTerminalReceipt(input: {
|
|
197
|
+
readonly graph: AIDataWorkflowRunGraph;
|
|
198
|
+
readonly nodeId: string;
|
|
199
|
+
readonly receipt: AIDataWorkflowChildTerminalReceipt;
|
|
200
|
+
}): AIDataWorkflowRunGraph {
|
|
201
|
+
const node = input.graph.nodes[input.nodeId];
|
|
202
|
+
if (!node?.childInvocation || !node.childFreezeReceipt) {
|
|
203
|
+
throw new AIDataWorkflowChildInvocationError(`Node ${input.nodeId} is not frozen for child settlement.`);
|
|
204
|
+
}
|
|
205
|
+
const receipt = normalizeAIDataWorkflowChildTerminalReceipt(input.receipt, node.childInvocation);
|
|
206
|
+
if (node.childTerminalReceipt) {
|
|
207
|
+
if (JSON.stringify(node.childTerminalReceipt) === JSON.stringify(receipt)) return input.graph;
|
|
208
|
+
throw new AIDataWorkflowChildInvocationError(`Node ${input.nodeId} already accepted another child terminal receipt.`);
|
|
209
|
+
}
|
|
210
|
+
if (node.status !== 'Running') {
|
|
211
|
+
throw new AIDataWorkflowChildInvocationError(`Node ${input.nodeId} must be Running before child settlement.`);
|
|
212
|
+
}
|
|
213
|
+
if (receipt.status === 'Succeeded') {
|
|
214
|
+
const expected = [...node.outputs].sort(compareCodeUnits);
|
|
215
|
+
const actual = Object.keys(receipt.output).sort(compareCodeUnits);
|
|
216
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
217
|
+
throw new AIDataWorkflowChildInvocationError(`Node ${input.nodeId} child output does not match its FlowContract.`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const result = receipt.status === 'Succeeded'
|
|
221
|
+
? { nodeId: node.id, generation: node.generation, status: 'Succeeded' as const, output: receipt.output }
|
|
222
|
+
: { nodeId: node.id, generation: node.generation, status: 'Failed' as const };
|
|
223
|
+
return Object.freeze({
|
|
224
|
+
...input.graph,
|
|
225
|
+
nodes: Object.freeze({
|
|
226
|
+
...input.graph.nodes,
|
|
227
|
+
[node.id]: Object.freeze({
|
|
228
|
+
...node,
|
|
229
|
+
status: receipt.status,
|
|
230
|
+
result: Object.freeze(result),
|
|
231
|
+
childTerminalReceipt: receipt,
|
|
232
|
+
}),
|
|
233
|
+
}),
|
|
234
|
+
invalidations: Object.freeze(input.graph.invalidations.filter((fact) => fact.nodeId !== node.id)),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function normalizeAIDataWorkflowChildInvocationIdentity(
|
|
239
|
+
value: unknown,
|
|
240
|
+
location = 'childInvocation.identity',
|
|
241
|
+
): AIDataWorkflowChildInvocationIdentity {
|
|
242
|
+
const input = record(value, [
|
|
243
|
+
'schemaVersion', 'invocationKey', 'parentInstanceId', 'parentRunId', 'parentNodeId', 'parentGeneration',
|
|
244
|
+
'childInstanceId', 'childRunId', 'subflow',
|
|
245
|
+
], location);
|
|
246
|
+
schema(input.schemaVersion, `${location}.schemaVersion`);
|
|
247
|
+
if (!Number.isSafeInteger(input.parentGeneration) || Number(input.parentGeneration) < 0) {
|
|
248
|
+
throw new AIDataWorkflowChildInvocationError(`${location}.parentGeneration must be a non-negative safe integer.`);
|
|
249
|
+
}
|
|
250
|
+
return Object.freeze({
|
|
251
|
+
schemaVersion: AI_DATA_CHILD_INVOCATION_SCHEMA_VERSION,
|
|
252
|
+
invocationKey: nonEmpty(input.invocationKey, `${location}.invocationKey`),
|
|
253
|
+
parentInstanceId: nonEmpty(input.parentInstanceId, `${location}.parentInstanceId`),
|
|
254
|
+
parentRunId: nonEmpty(input.parentRunId, `${location}.parentRunId`),
|
|
255
|
+
parentNodeId: nonEmpty(input.parentNodeId, `${location}.parentNodeId`),
|
|
256
|
+
parentGeneration: Number(input.parentGeneration),
|
|
257
|
+
childInstanceId: nonEmpty(input.childInstanceId, `${location}.childInstanceId`),
|
|
258
|
+
childRunId: nonEmpty(input.childRunId, `${location}.childRunId`),
|
|
259
|
+
subflow: normalizeSubflow(input.subflow, `${location}.subflow`),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function normalizeAIDataWorkflowChildFreezeReceipt(
|
|
264
|
+
value: unknown,
|
|
265
|
+
identity: AIDataWorkflowChildInvocationIdentity,
|
|
266
|
+
location = 'childInvocation.freeze',
|
|
267
|
+
): AIDataWorkflowChildFreezeReceipt {
|
|
268
|
+
const input = record(value, [
|
|
269
|
+
'schemaVersion', 'invocationKey', 'childInstanceId', 'childRunId', 'definitionRef', 'flowRef',
|
|
270
|
+
'contractDigest', 'definitionRevision', 'definitionDigest',
|
|
271
|
+
], location);
|
|
272
|
+
schema(input.schemaVersion, `${location}.schemaVersion`);
|
|
273
|
+
const normalized = Object.freeze({
|
|
274
|
+
schemaVersion: AI_DATA_CHILD_INVOCATION_SCHEMA_VERSION,
|
|
275
|
+
invocationKey: nonEmpty(input.invocationKey, `${location}.invocationKey`),
|
|
276
|
+
childInstanceId: nonEmpty(input.childInstanceId, `${location}.childInstanceId`),
|
|
277
|
+
childRunId: nonEmpty(input.childRunId, `${location}.childRunId`),
|
|
278
|
+
definitionRef: prefixed(input.definitionRef, 'resource://', `${location}.definitionRef`) as `resource://${string}`,
|
|
279
|
+
flowRef: prefixed(input.flowRef, 'eager-data-flow://', `${location}.flowRef`) as `eager-data-flow://${string}`,
|
|
280
|
+
contractDigest: nonEmpty(input.contractDigest, `${location}.contractDigest`),
|
|
281
|
+
definitionRevision: nonEmpty(input.definitionRevision, `${location}.definitionRevision`),
|
|
282
|
+
definitionDigest: nonEmpty(input.definitionDigest, `${location}.definitionDigest`),
|
|
283
|
+
});
|
|
284
|
+
assertChildIdentity(normalized, identity, location);
|
|
285
|
+
return normalized;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function normalizeAIDataWorkflowChildInvocationObservation(
|
|
289
|
+
value: unknown,
|
|
290
|
+
identity: AIDataWorkflowChildInvocationIdentity,
|
|
291
|
+
location = 'childInvocation.observation',
|
|
292
|
+
): AIDataWorkflowChildInvocationObservation {
|
|
293
|
+
const input = record(value, ['schemaVersion', 'invocationKey', 'childInstanceId', 'childRunId', 'status'], location);
|
|
294
|
+
schema(input.schemaVersion, `${location}.schemaVersion`);
|
|
295
|
+
if (input.status !== 'Reserved' && input.status !== 'Running'
|
|
296
|
+
&& input.status !== 'Succeeded' && input.status !== 'Failed') {
|
|
297
|
+
throw new AIDataWorkflowChildInvocationError(`${location}.status is invalid.`);
|
|
298
|
+
}
|
|
299
|
+
const normalized = Object.freeze({
|
|
300
|
+
schemaVersion: AI_DATA_CHILD_INVOCATION_SCHEMA_VERSION,
|
|
301
|
+
invocationKey: nonEmpty(input.invocationKey, `${location}.invocationKey`),
|
|
302
|
+
childInstanceId: nonEmpty(input.childInstanceId, `${location}.childInstanceId`),
|
|
303
|
+
childRunId: nonEmpty(input.childRunId, `${location}.childRunId`),
|
|
304
|
+
status: input.status,
|
|
305
|
+
});
|
|
306
|
+
assertChildIdentity(normalized, identity, location);
|
|
307
|
+
return normalized;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function normalizeAIDataWorkflowChildTerminalReceipt(
|
|
311
|
+
value: unknown,
|
|
312
|
+
identity: AIDataWorkflowChildInvocationIdentity,
|
|
313
|
+
location = 'childInvocation.terminalReceipt',
|
|
314
|
+
): AIDataWorkflowChildTerminalReceipt {
|
|
315
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
316
|
+
throw new AIDataWorkflowChildInvocationError(`${location} must be an object.`);
|
|
317
|
+
}
|
|
318
|
+
const status = (value as Readonly<Record<string, unknown>>).status;
|
|
319
|
+
const common = [
|
|
320
|
+
'schemaVersion', 'invocationKey', 'childInstanceId', 'childRunId', 'definitionRef', 'flowRef',
|
|
321
|
+
'contractDigest', 'status', 'receiptDigest',
|
|
322
|
+
];
|
|
323
|
+
const input = record(value, status === 'Succeeded'
|
|
324
|
+
? [...common, 'output']
|
|
325
|
+
: status === 'Failed'
|
|
326
|
+
? [...common, 'failureCode', 'failureMessage']
|
|
327
|
+
: common, location);
|
|
328
|
+
schema(input.schemaVersion, `${location}.schemaVersion`);
|
|
329
|
+
if (status !== 'Succeeded' && status !== 'Failed') {
|
|
330
|
+
throw new AIDataWorkflowChildInvocationError(`${location}.status must be terminal.`);
|
|
331
|
+
}
|
|
332
|
+
const commonResult = {
|
|
333
|
+
schemaVersion: AI_DATA_CHILD_INVOCATION_SCHEMA_VERSION,
|
|
334
|
+
invocationKey: nonEmpty(input.invocationKey, `${location}.invocationKey`),
|
|
335
|
+
childInstanceId: nonEmpty(input.childInstanceId, `${location}.childInstanceId`),
|
|
336
|
+
childRunId: nonEmpty(input.childRunId, `${location}.childRunId`),
|
|
337
|
+
definitionRef: prefixed(input.definitionRef, 'resource://', `${location}.definitionRef`) as `resource://${string}`,
|
|
338
|
+
flowRef: prefixed(input.flowRef, 'eager-data-flow://', `${location}.flowRef`) as `eager-data-flow://${string}`,
|
|
339
|
+
contractDigest: nonEmpty(input.contractDigest, `${location}.contractDigest`),
|
|
340
|
+
receiptDigest: nonEmpty(input.receiptDigest, `${location}.receiptDigest`),
|
|
341
|
+
};
|
|
342
|
+
assertChildIdentity(commonResult, identity, location);
|
|
343
|
+
if (status === 'Succeeded') {
|
|
344
|
+
return Object.freeze({
|
|
345
|
+
...commonResult,
|
|
346
|
+
status,
|
|
347
|
+
output: normalizeFlowClosedObject(input.output, `${location}.output`),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return Object.freeze({
|
|
351
|
+
...commonResult,
|
|
352
|
+
status,
|
|
353
|
+
failureCode: nonEmpty(input.failureCode, `${location}.failureCode`),
|
|
354
|
+
failureMessage: nonEmpty(input.failureMessage, `${location}.failureMessage`),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function assertChildIdentity(
|
|
359
|
+
value: Readonly<Record<string, unknown>>,
|
|
360
|
+
identity: AIDataWorkflowChildInvocationIdentity,
|
|
361
|
+
location: string,
|
|
362
|
+
): void {
|
|
363
|
+
const expected: Readonly<Record<string, unknown>> = {
|
|
364
|
+
invocationKey: identity.invocationKey,
|
|
365
|
+
childInstanceId: identity.childInstanceId,
|
|
366
|
+
childRunId: identity.childRunId,
|
|
367
|
+
definitionRef: identity.subflow.definitionRef,
|
|
368
|
+
flowRef: identity.subflow.flowRef,
|
|
369
|
+
contractDigest: identity.subflow.contractDigest,
|
|
370
|
+
};
|
|
371
|
+
for (const [field, expectedValue] of Object.entries(expected)) {
|
|
372
|
+
if (value[field] !== undefined && value[field] !== expectedValue) {
|
|
373
|
+
throw new AIDataWorkflowChildInvocationError(`${location}.${field} does not match the reserved child identity.`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
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,
|
|
@@ -63,6 +66,7 @@ import { assertFrozenAIAgentTaskBinding } from 'ai-workflow-logic/run-freeze';
|
|
|
63
66
|
|
|
64
67
|
export * from './holon-task.js';
|
|
65
68
|
export * from './autonomous-control.js';
|
|
69
|
+
export * from './child-invocation.js';
|
|
66
70
|
|
|
67
71
|
export interface CreateAIDataWorkflowCheckpointInput {
|
|
68
72
|
readonly instanceId: string;
|
|
@@ -287,12 +291,20 @@ function cloneInvalidation(invalidation: AIDataWorkflowInvalidation): AIDataWork
|
|
|
287
291
|
return { ...invalidation };
|
|
288
292
|
}
|
|
289
293
|
|
|
294
|
+
function cloneSubflow(binding: AIDataWorkflowSubFlowBinding): AIDataWorkflowSubFlowBinding {
|
|
295
|
+
return { ...binding };
|
|
296
|
+
}
|
|
297
|
+
|
|
290
298
|
function cloneNode(node: AIDataWorkflowRunNode): AIDataWorkflowRunNode {
|
|
291
299
|
return {
|
|
292
300
|
...node,
|
|
293
301
|
inputs: cloneRecord(node.inputs),
|
|
294
302
|
outputs: [...node.outputs],
|
|
295
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) } : {}),
|
|
296
308
|
dependsOn: [...node.dependsOn],
|
|
297
309
|
dependents: [...node.dependents],
|
|
298
310
|
reusePolicy: { ...node.reusePolicy },
|
|
@@ -302,6 +314,16 @@ function cloneNode(node: AIDataWorkflowRunNode): AIDataWorkflowRunNode {
|
|
|
302
314
|
};
|
|
303
315
|
}
|
|
304
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
|
+
|
|
305
327
|
function cloneNodeResult(result: AIWorkflowNodeResult): AIWorkflowNodeResult {
|
|
306
328
|
return {
|
|
307
329
|
...result,
|
|
@@ -326,7 +348,27 @@ function cloneGraph(graph: AIDataWorkflowRunGraph): AIDataWorkflowRunGraph {
|
|
|
326
348
|
declarationOrder: [...graph.declarationOrder],
|
|
327
349
|
patchHistory: graph.patchHistory.map((record) => ({
|
|
328
350
|
...record,
|
|
329
|
-
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
|
+
}),
|
|
330
372
|
})),
|
|
331
373
|
invalidations: graph.invalidations.map(cloneInvalidation),
|
|
332
374
|
};
|
|
@@ -354,6 +396,40 @@ function nodeConfig(node: EagerDataFlowNodePlan): AIDataWorkflowNodeConfig {
|
|
|
354
396
|
return {};
|
|
355
397
|
}
|
|
356
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
|
+
|
|
357
433
|
function configReusePolicy(config: AIDataWorkflowNodeConfig): AIWorkflowReusePolicy | undefined {
|
|
358
434
|
const raw = config.reuse_policy ?? config.reusePolicy;
|
|
359
435
|
return raw === 'semantic-hash' || raw === 'never' ? raw : undefined;
|
|
@@ -548,10 +624,12 @@ export function createAIDataWorkflowRunGraph(
|
|
|
548
624
|
inputs: nodeInputs(planNode),
|
|
549
625
|
outputs: nodeOutputs(planNode),
|
|
550
626
|
config,
|
|
627
|
+
...(nodeSubflow(planNode) ? { subflow: nodeSubflow(planNode) } : {}),
|
|
551
628
|
dependsOn: dependencies.get(planNode.id) ?? [],
|
|
552
629
|
dependents: [],
|
|
553
630
|
reusePolicy,
|
|
554
631
|
};
|
|
632
|
+
assertSubflowBinding(runNode.tag, runNode.subflow, runNode.id);
|
|
555
633
|
return [planNode.id, runNode];
|
|
556
634
|
}),
|
|
557
635
|
);
|
|
@@ -574,6 +652,96 @@ export function createAIDataWorkflowRunGraph(
|
|
|
574
652
|
return graph;
|
|
575
653
|
}
|
|
576
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
|
+
|
|
577
745
|
function makePatchNode(
|
|
578
746
|
spec: AIDataWorkflowGraphPatchNodeSpec,
|
|
579
747
|
generation: number,
|
|
@@ -588,6 +756,8 @@ function makePatchNode(
|
|
|
588
756
|
platformDefaultPolicy,
|
|
589
757
|
});
|
|
590
758
|
assertReusePolicyAuthority(reusePolicy, nodeType, config, platformDefaultPolicy, spec.id);
|
|
759
|
+
const subflow = spec.subflow ? cloneSubflow(spec.subflow) : undefined;
|
|
760
|
+
assertSubflowBinding(tag, subflow, spec.id);
|
|
591
761
|
return {
|
|
592
762
|
id: spec.id,
|
|
593
763
|
tag,
|
|
@@ -598,6 +768,7 @@ function makePatchNode(
|
|
|
598
768
|
inputs: cloneRecord(spec.inputs),
|
|
599
769
|
outputs: [...(spec.outputs ?? [])],
|
|
600
770
|
config,
|
|
771
|
+
...(subflow ? { subflow } : {}),
|
|
601
772
|
dependsOn: uniqueStrings(spec.dependsOn ?? []),
|
|
602
773
|
dependents: [],
|
|
603
774
|
reusePolicy,
|
|
@@ -663,6 +834,9 @@ function applyInvalidations(
|
|
|
663
834
|
const {
|
|
664
835
|
result: _staleResult,
|
|
665
836
|
semanticFingerprint: _staleFingerprint,
|
|
837
|
+
childInvocation: _staleChildInvocation,
|
|
838
|
+
childFreezeReceipt: _staleChildFreezeReceipt,
|
|
839
|
+
childTerminalReceipt: _staleChildTerminalReceipt,
|
|
666
840
|
...currentFacts
|
|
667
841
|
} = node;
|
|
668
842
|
nextNodes = {
|
|
@@ -783,6 +957,13 @@ export function applyAIDataWorkflowGraphPatch(
|
|
|
783
957
|
);
|
|
784
958
|
}
|
|
785
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);
|
|
786
967
|
const nodeType = operation.changes.nodeType ?? existing.nodeType;
|
|
787
968
|
const dependsOn = operation.changes.dependsOn
|
|
788
969
|
? uniqueStrings(operation.changes.dependsOn)
|
|
@@ -798,17 +979,26 @@ export function applyAIDataWorkflowGraphPatch(
|
|
|
798
979
|
: existing.reusePolicy
|
|
799
980
|
);
|
|
800
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;
|
|
801
990
|
nodes = {
|
|
802
991
|
...nodes,
|
|
803
992
|
[existing.id]: {
|
|
804
|
-
...
|
|
805
|
-
...
|
|
806
|
-
tag
|
|
993
|
+
...existingWithoutChild,
|
|
994
|
+
...changesWithoutSubflow,
|
|
995
|
+
tag,
|
|
807
996
|
nodeType,
|
|
808
997
|
generation,
|
|
809
998
|
inputs: operation.changes.inputs ? cloneRecord(operation.changes.inputs) : existing.inputs,
|
|
810
999
|
outputs: operation.changes.outputs ? [...operation.changes.outputs] : existing.outputs,
|
|
811
1000
|
config,
|
|
1001
|
+
...(subflow ? { subflow } : {}),
|
|
812
1002
|
dependsOn,
|
|
813
1003
|
reusePolicy,
|
|
814
1004
|
},
|
|
@@ -834,6 +1024,9 @@ export function applyAIDataWorkflowGraphPatch(
|
|
|
834
1024
|
result: _staleResult,
|
|
835
1025
|
semanticFingerprint: _staleFingerprint,
|
|
836
1026
|
invalidatedBy: _activeInvalidation,
|
|
1027
|
+
childInvocation: _staleChildInvocation,
|
|
1028
|
+
childFreezeReceipt: _staleChildFreezeReceipt,
|
|
1029
|
+
childTerminalReceipt: _staleChildTerminalReceipt,
|
|
837
1030
|
...currentFacts
|
|
838
1031
|
} = existing;
|
|
839
1032
|
nodes = {
|