ai-data-workflow-logic 0.1.4 → 0.1.6
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 +11 -9
- package/src/filesystem.ts +446 -0
- package/src/index.ts +298 -19
package/package.json
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-data-workflow-logic",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "EagerDataFlow-backed AI Data Workflow RunGraph helpers",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.ts",
|
|
10
|
-
"./browser": "./src/browser.ts"
|
|
10
|
+
"./browser": "./src/browser.ts",
|
|
11
|
+
"./filesystem": "./src/filesystem.ts"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
|
-
"ai-data-workflow-contract": "0.1.
|
|
14
|
-
"ai-workflow-contract": "0.1.
|
|
15
|
-
"ai-workflow-logic": "0.1.
|
|
16
|
-
"eager-data-flow-contract": "0.1.
|
|
17
|
-
"eager-data-flow-logic": "
|
|
14
|
+
"ai-data-workflow-contract": "0.1.2",
|
|
15
|
+
"ai-workflow-contract": "0.1.5",
|
|
16
|
+
"ai-workflow-logic": "0.1.5",
|
|
17
|
+
"eager-data-flow-contract": "0.1.2",
|
|
18
|
+
"eager-data-flow-logic": "0.1.3",
|
|
19
|
+
"flow-step-space-logic": "0.1.0",
|
|
20
|
+
"work-ctrl-flow-logic": "0.1.2"
|
|
18
21
|
},
|
|
19
22
|
"files": [
|
|
20
23
|
"src"
|
|
21
24
|
],
|
|
22
25
|
"publishConfig": {
|
|
23
|
-
"access": "public"
|
|
24
|
-
"registry": "https://registry.npmjs.com"
|
|
26
|
+
"access": "public"
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type {
|
|
4
|
+
AIDataWorkflowRunCheckpoint,
|
|
5
|
+
AIDataWorkflowRunGraph,
|
|
6
|
+
} from 'ai-data-workflow-contract';
|
|
7
|
+
import type {
|
|
8
|
+
AIDataWorkflowRuntime,
|
|
9
|
+
AIAgentHostEffectPort,
|
|
10
|
+
FrozenAIAgentTaskBinding,
|
|
11
|
+
AIWorkflowProfileDurableState,
|
|
12
|
+
AIWorkflowReusePolicy,
|
|
13
|
+
FlowClosedValue,
|
|
14
|
+
} from 'ai-workflow-contract';
|
|
15
|
+
import type {
|
|
16
|
+
EagerDataFlowModuleImporter,
|
|
17
|
+
} from 'eager-data-flow-logic';
|
|
18
|
+
import type { EagerDataFlowDurableNodeState } from 'eager-data-flow-contract';
|
|
19
|
+
import {
|
|
20
|
+
createFilesystemCodeResolver,
|
|
21
|
+
materializeEagerDataFlowPlan,
|
|
22
|
+
} from 'eager-data-flow-logic';
|
|
23
|
+
import { assembleDefinitionStepProfileDirectory } from 'flow-step-space-logic/filesystem';
|
|
24
|
+
import { FilesystemFlowRunCheckpointStore } from 'ai-workflow-logic/checkpoint-filesystem';
|
|
25
|
+
import {
|
|
26
|
+
normalizeAIWorkflowProfileDurableState,
|
|
27
|
+
normalizeFlowClosedObject,
|
|
28
|
+
normalizeFlowClosedValue,
|
|
29
|
+
} from 'ai-workflow-logic';
|
|
30
|
+
import {
|
|
31
|
+
loadMaterializedFlowInstance,
|
|
32
|
+
materializeFlowInstance,
|
|
33
|
+
type FlowInstanceSourceIdentity,
|
|
34
|
+
} from 'work-ctrl-flow-logic';
|
|
35
|
+
import {
|
|
36
|
+
commitAIDataWorkflowCheckpoint,
|
|
37
|
+
createAIDataWorkflowCheckpoint,
|
|
38
|
+
createAIDataWorkflowRunGraph,
|
|
39
|
+
createAIDataWorkflowAgentNodeRuntimeBinder,
|
|
40
|
+
loadAIDataWorkflowCheckpoint,
|
|
41
|
+
loadAIDataWorkflowSources,
|
|
42
|
+
recordAIDataWorkflowNodeResult,
|
|
43
|
+
} from './index.js';
|
|
44
|
+
|
|
45
|
+
export interface MaterializedAIDataWorkflowRuntime {
|
|
46
|
+
readonly supportRoot: string;
|
|
47
|
+
readonly workflow: AIDataWorkflowRuntime;
|
|
48
|
+
readonly importModule: EagerDataFlowModuleImporter;
|
|
49
|
+
readonly agentEffects?: AIAgentHostEffectPort;
|
|
50
|
+
readonly agentTaskProofs?: Readonly<Record<string, FrozenAIAgentTaskBinding>>;
|
|
51
|
+
readonly checkpointObserver?: (event: {
|
|
52
|
+
readonly checkpoint: AIDataWorkflowRunCheckpoint;
|
|
53
|
+
readonly nodeId: string;
|
|
54
|
+
readonly phase: 'before' | 'after' | 'failed';
|
|
55
|
+
}) => void | Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface StartMaterializedAIDataWorkflowInput {
|
|
59
|
+
readonly instanceId: string;
|
|
60
|
+
readonly runId: string;
|
|
61
|
+
readonly bundleDir?: string;
|
|
62
|
+
readonly source?: FlowInstanceSourceIdentity;
|
|
63
|
+
readonly input: Readonly<Record<string, unknown>>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface StartMaterializedAIDataWorkflowConfig {
|
|
67
|
+
readonly checkpointConfig: FlowClosedValue;
|
|
68
|
+
readonly platformDefaultPolicy?: AIWorkflowReusePolicy;
|
|
69
|
+
readonly initialState?: FlowClosedValue;
|
|
70
|
+
readonly durableAI: AIWorkflowProfileDurableState;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface MaterializedAIDataWorkflowResult {
|
|
74
|
+
readonly checkpoint: AIDataWorkflowRunCheckpoint;
|
|
75
|
+
readonly graph: AIDataWorkflowRunGraph;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ContinueMaterializedAIDataWorkflowInput {
|
|
79
|
+
readonly instanceId: string;
|
|
80
|
+
readonly runId: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ContinueMaterializedAIDataWorkflowConfig = Readonly<Record<never, never>>;
|
|
84
|
+
|
|
85
|
+
/** Runtime-first canonical Data execution from a frozen instance. */
|
|
86
|
+
export async function startMaterializedAIDataWorkflow(
|
|
87
|
+
runtime: MaterializedAIDataWorkflowRuntime,
|
|
88
|
+
input: StartMaterializedAIDataWorkflowInput,
|
|
89
|
+
config: StartMaterializedAIDataWorkflowConfig,
|
|
90
|
+
): Promise<MaterializedAIDataWorkflowResult> {
|
|
91
|
+
requireAgentTaskBindings(runtime);
|
|
92
|
+
const instance = ensureAIDataInstance(runtime, input);
|
|
93
|
+
const binding = loadAIDataBinding(instance.definitionDir);
|
|
94
|
+
const checkpointStore = new FilesystemFlowRunCheckpointStore({ storeRoot: runtime.supportRoot });
|
|
95
|
+
const checkpointRuntime = { checkpointStore };
|
|
96
|
+
const existing = await loadAIDataWorkflowCheckpoint(checkpointRuntime, input);
|
|
97
|
+
if (existing) {
|
|
98
|
+
assertStartMatchesCheckpoint(existing, input, config);
|
|
99
|
+
return executeAIDataCheckpoint(runtime, binding, existing);
|
|
100
|
+
}
|
|
101
|
+
const graph = createAIDataWorkflowRunGraph({
|
|
102
|
+
binding,
|
|
103
|
+
runId: input.runId,
|
|
104
|
+
platformDefaultPolicy: config.platformDefaultPolicy,
|
|
105
|
+
});
|
|
106
|
+
const initialState = { status: 'Pending', generation: graph.currentGeneration } as const;
|
|
107
|
+
if (config.initialState !== undefined
|
|
108
|
+
&& JSON.stringify(normalizeFlowClosedValue(config.initialState, 'aiData.start.state')) !== JSON.stringify(initialState)) {
|
|
109
|
+
throw new Error('AI Data Workflow initialState must equal the canonical Pending RunGraph projection.');
|
|
110
|
+
}
|
|
111
|
+
const v0 = await createAIDataWorkflowCheckpoint(checkpointRuntime, {
|
|
112
|
+
instanceId: input.instanceId,
|
|
113
|
+
definition: instance.descriptor.definition,
|
|
114
|
+
graph: durableGraph(graph),
|
|
115
|
+
input: input.input as FlowClosedValue,
|
|
116
|
+
config: config.checkpointConfig,
|
|
117
|
+
state: initialState,
|
|
118
|
+
output: null,
|
|
119
|
+
controllerSidecars: { status: 'Pending' },
|
|
120
|
+
nodeSidecars: nodeStatuses(graph),
|
|
121
|
+
ai: config.durableAI,
|
|
122
|
+
});
|
|
123
|
+
return executeAIDataCheckpoint(runtime, binding, v0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Runtime-first continuation from a checkpoint reconstructed in a fresh process. */
|
|
127
|
+
export async function continueMaterializedAIDataWorkflow(
|
|
128
|
+
runtime: MaterializedAIDataWorkflowRuntime,
|
|
129
|
+
input: ContinueMaterializedAIDataWorkflowInput,
|
|
130
|
+
_config: ContinueMaterializedAIDataWorkflowConfig,
|
|
131
|
+
): Promise<MaterializedAIDataWorkflowResult> {
|
|
132
|
+
requireAgentTaskBindings(runtime);
|
|
133
|
+
const instanceDir = path.join(runtime.supportRoot, 'instances', input.instanceId);
|
|
134
|
+
const instance = loadMaterializedFlowInstance(instanceDir, input.instanceId, validateAIDataDefinition);
|
|
135
|
+
const binding = loadAIDataBinding(instance.definitionDir);
|
|
136
|
+
const checkpoint = await loadAIDataWorkflowCheckpoint(
|
|
137
|
+
{ checkpointStore: new FilesystemFlowRunCheckpointStore({ storeRoot: runtime.supportRoot }) },
|
|
138
|
+
input,
|
|
139
|
+
);
|
|
140
|
+
if (!checkpoint) throw new Error(`AI Data Workflow run ${input.runId} does not have a canonical checkpoint.`);
|
|
141
|
+
return executeAIDataCheckpoint(runtime, binding, checkpoint);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function requireAgentTaskBindings(runtime: MaterializedAIDataWorkflowRuntime): void {
|
|
145
|
+
if (runtime.agentEffects && (!runtime.agentTaskProofs || Object.keys(runtime.agentTaskProofs).length === 0)) {
|
|
146
|
+
throw new Error('AI_AGENT_TASK_PROOF_REQUIRED: materialized AI Data Agent effects require run-freeze task proofs.');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function executeAIDataCheckpoint(
|
|
151
|
+
runtime: MaterializedAIDataWorkflowRuntime,
|
|
152
|
+
binding: ReturnType<typeof loadAIDataBinding>,
|
|
153
|
+
checkpoint: AIDataWorkflowRunCheckpoint,
|
|
154
|
+
): Promise<MaterializedAIDataWorkflowResult> {
|
|
155
|
+
let accepted = checkpoint;
|
|
156
|
+
let graph: AIDataWorkflowRunGraph = {
|
|
157
|
+
...checkpoint.profile.runGraph,
|
|
158
|
+
binding,
|
|
159
|
+
};
|
|
160
|
+
const status = checkpointStatus(checkpoint);
|
|
161
|
+
if (status === 'Succeeded' || status === 'Failed') return { checkpoint, graph };
|
|
162
|
+
if (status !== 'Pending' && status !== 'Running') {
|
|
163
|
+
throw new Error(`AI Data Workflow checkpoint ${checkpoint.runId} has unsupported continuation status ${String(status)}.`);
|
|
164
|
+
}
|
|
165
|
+
if (typeof checkpoint.input !== 'object' || checkpoint.input === null || Array.isArray(checkpoint.input)) {
|
|
166
|
+
throw new Error(`AI Data Workflow checkpoint ${checkpoint.runId} input must be a closed object.`);
|
|
167
|
+
}
|
|
168
|
+
const invocationInput = checkpoint.input as Readonly<Record<string, FlowClosedValue>>;
|
|
169
|
+
const checkpointRuntime = {
|
|
170
|
+
checkpointStore: new FilesystemFlowRunCheckpointStore({ storeRoot: runtime.supportRoot }),
|
|
171
|
+
};
|
|
172
|
+
let transitionTail = Promise.resolve();
|
|
173
|
+
const transition = <T>(operation: () => Promise<T>): Promise<T> => {
|
|
174
|
+
const result = transitionTail.then(operation);
|
|
175
|
+
transitionTail = result.then(() => undefined, () => undefined);
|
|
176
|
+
return result;
|
|
177
|
+
};
|
|
178
|
+
const commitGraph = (
|
|
179
|
+
updateGraph: (current: AIDataWorkflowRunGraph) => AIDataWorkflowRunGraph,
|
|
180
|
+
nodeId: string,
|
|
181
|
+
phase: 'before' | 'after' | 'failed',
|
|
182
|
+
): Promise<void> => transition(async () => {
|
|
183
|
+
const owner = await loadAIDataWorkflowCheckpoint(checkpointRuntime, {
|
|
184
|
+
instanceId: checkpoint.instanceId,
|
|
185
|
+
runId: checkpoint.runId,
|
|
186
|
+
});
|
|
187
|
+
if (!owner) throw new Error(`AI Data Workflow checkpoint ${checkpoint.runId} disappeared during node transition.`);
|
|
188
|
+
accepted = owner;
|
|
189
|
+
graph = { ...owner.profile.runGraph, binding };
|
|
190
|
+
const nextGraph = updateGraph(graph);
|
|
191
|
+
const nextStatus = graphCheckpointStatus(nextGraph);
|
|
192
|
+
accepted = await commitAIDataWorkflowCheckpoint(checkpointRuntime, {
|
|
193
|
+
expectedVersion: accepted.version,
|
|
194
|
+
checkpoint: {
|
|
195
|
+
...accepted,
|
|
196
|
+
version: accepted.version + 1,
|
|
197
|
+
state: { status: nextStatus, generation: nextGraph.currentGeneration },
|
|
198
|
+
output: graphCheckpointOutput(nextGraph, nextStatus),
|
|
199
|
+
controllerSidecars: { status: nextStatus },
|
|
200
|
+
nodeSidecars: nodeStatuses(nextGraph),
|
|
201
|
+
profile: { ...accepted.profile, runGraph: durableGraph(nextGraph) },
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
graph = { ...accepted.profile.runGraph, binding };
|
|
205
|
+
if (runtime.checkpointObserver) {
|
|
206
|
+
try {
|
|
207
|
+
await runtime.checkpointObserver({ checkpoint: accepted, nodeId, phase });
|
|
208
|
+
} catch (cause) {
|
|
209
|
+
throw new AIDataCheckpointObserverError(cause);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
let output: unknown;
|
|
214
|
+
try {
|
|
215
|
+
const executable = await materializeEagerDataFlowPlan(binding.definition, {
|
|
216
|
+
resolveCode: createFilesystemCodeResolver(runtime.importModule),
|
|
217
|
+
});
|
|
218
|
+
output = await executable.invoke(runtime.workflow, invocationInput, {
|
|
219
|
+
completedNodes: completedEagerNodes(graph),
|
|
220
|
+
...(runtime.agentEffects ? {
|
|
221
|
+
bindNode: ((binder) => (nodeRuntime: unknown, identity: { readonly nodeId: string }) => binder(
|
|
222
|
+
nodeRuntime as AIDataWorkflowRuntime,
|
|
223
|
+
identity,
|
|
224
|
+
))(createAIDataWorkflowAgentNodeRuntimeBinder({
|
|
225
|
+
flowInstanceId: checkpoint.instanceId,
|
|
226
|
+
runId: checkpoint.runId,
|
|
227
|
+
checkpointRuntime,
|
|
228
|
+
effects: runtime.agentEffects,
|
|
229
|
+
workflowRef: `resource://${binding.definition.fqn}`,
|
|
230
|
+
taskProofs: runtime.agentTaskProofs ?? {},
|
|
231
|
+
generationForNode: (nodeId) => graph.nodes[nodeId]?.generation ?? 0,
|
|
232
|
+
})),
|
|
233
|
+
} : {}),
|
|
234
|
+
onNodeStart: (nodeId) => {
|
|
235
|
+
return commitGraph((current) => {
|
|
236
|
+
const node = current.nodes[nodeId];
|
|
237
|
+
if (!node) throw new Error(`AI Data Workflow node ${nodeId} does not exist in the durable graph.`);
|
|
238
|
+
return recordAIDataWorkflowNodeResult(current, nodeId, {
|
|
239
|
+
nodeId,
|
|
240
|
+
generation: node.generation,
|
|
241
|
+
status: 'Running',
|
|
242
|
+
});
|
|
243
|
+
}, nodeId, 'before');
|
|
244
|
+
},
|
|
245
|
+
onNodeSuccess: (settlement) => {
|
|
246
|
+
return commitGraph((current) => {
|
|
247
|
+
const node = current.nodes[settlement.nodeId];
|
|
248
|
+
if (!node) throw new Error(`AI Data Workflow node ${settlement.nodeId} does not exist in the durable graph.`);
|
|
249
|
+
return recordAIDataWorkflowNodeResult(current, settlement.nodeId, {
|
|
250
|
+
nodeId: settlement.nodeId,
|
|
251
|
+
generation: node.generation,
|
|
252
|
+
status: 'Succeeded',
|
|
253
|
+
...(settlement.output !== undefined ? { output: settlement.output } : {}),
|
|
254
|
+
...(settlement.returned !== undefined ? { output: settlement.returned } : {}),
|
|
255
|
+
});
|
|
256
|
+
}, settlement.nodeId, 'after');
|
|
257
|
+
},
|
|
258
|
+
onNodeFailure: ({ nodeId }) => {
|
|
259
|
+
return commitGraph((current) => {
|
|
260
|
+
const node = current.nodes[nodeId];
|
|
261
|
+
if (!node) throw new Error(`AI Data Workflow node ${nodeId} does not exist in the durable graph.`);
|
|
262
|
+
return recordAIDataWorkflowNodeResult(current, nodeId, {
|
|
263
|
+
nodeId,
|
|
264
|
+
generation: node.generation,
|
|
265
|
+
status: 'Failed',
|
|
266
|
+
});
|
|
267
|
+
}, nodeId, 'failed');
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
const observerError = findCheckpointObserverError(error);
|
|
272
|
+
if (observerError) throw observerError.cause;
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
if (checkpointStatus(accepted) !== 'Succeeded') {
|
|
276
|
+
throw new Error(`AI Data Workflow ${checkpoint.runId} completed without a terminal RunGraph projection.`);
|
|
277
|
+
}
|
|
278
|
+
if (JSON.stringify(accepted.output) !== JSON.stringify(normalizeFlowClosedValue(output, 'aiData.execution.output'))) {
|
|
279
|
+
throw new Error(`AI Data Workflow ${checkpoint.runId} Return output does not match its durable RunGraph result.`);
|
|
280
|
+
}
|
|
281
|
+
return { checkpoint: accepted, graph };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
class AIDataCheckpointObserverError extends Error {
|
|
285
|
+
constructor(public override readonly cause: unknown) {
|
|
286
|
+
super('AI Data checkpoint observer interrupted execution.');
|
|
287
|
+
this.name = 'AIDataCheckpointObserverError';
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function findCheckpointObserverError(value: unknown): AIDataCheckpointObserverError | undefined {
|
|
292
|
+
if (value instanceof AIDataCheckpointObserverError) return value;
|
|
293
|
+
if (typeof value !== 'object' || value === null || !('cause' in value)) return undefined;
|
|
294
|
+
return findCheckpointObserverError(value.cause);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function completedEagerNodes(
|
|
298
|
+
graph: AIDataWorkflowRunGraph,
|
|
299
|
+
): Readonly<Record<string, EagerDataFlowDurableNodeState>> {
|
|
300
|
+
return Object.fromEntries(graph.declarationOrder.flatMap((nodeId) => {
|
|
301
|
+
const node = graph.nodes[nodeId];
|
|
302
|
+
if (!node || (node.status !== 'Succeeded' && node.status !== 'Reused') || !node.result) return [];
|
|
303
|
+
const state: EagerDataFlowDurableNodeState = {
|
|
304
|
+
nodeId,
|
|
305
|
+
...(node.tag === 'ReturnNode'
|
|
306
|
+
? { returned: node.result.output as Readonly<Record<string, unknown>> }
|
|
307
|
+
: node.tag === 'SinkNode'
|
|
308
|
+
? {}
|
|
309
|
+
: { output: node.result.output as Readonly<Record<string, unknown>> }),
|
|
310
|
+
};
|
|
311
|
+
return [[nodeId, state] as const];
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Fresh reconstruction reads only the frozen instance and canonical checkpoint. */
|
|
316
|
+
export async function loadMaterializedAIDataWorkflowRun(
|
|
317
|
+
runtime: MaterializedAIDataWorkflowRuntime,
|
|
318
|
+
input: Pick<StartMaterializedAIDataWorkflowInput, 'instanceId' | 'runId'>,
|
|
319
|
+
): Promise<MaterializedAIDataWorkflowResult | undefined> {
|
|
320
|
+
const instanceDir = path.join(runtime.supportRoot, 'instances', input.instanceId);
|
|
321
|
+
const instance = loadMaterializedFlowInstance(instanceDir, input.instanceId, validateAIDataDefinition);
|
|
322
|
+
const binding = loadAIDataBinding(instance.definitionDir);
|
|
323
|
+
const checkpoint = await loadAIDataWorkflowCheckpoint(
|
|
324
|
+
{ checkpointStore: new FilesystemFlowRunCheckpointStore({ storeRoot: runtime.supportRoot }) },
|
|
325
|
+
input,
|
|
326
|
+
);
|
|
327
|
+
if (!checkpoint) return undefined;
|
|
328
|
+
return {
|
|
329
|
+
checkpoint,
|
|
330
|
+
graph: {
|
|
331
|
+
...checkpoint.profile.runGraph,
|
|
332
|
+
binding,
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function ensureAIDataInstance(
|
|
338
|
+
runtime: MaterializedAIDataWorkflowRuntime,
|
|
339
|
+
input: StartMaterializedAIDataWorkflowInput,
|
|
340
|
+
) {
|
|
341
|
+
const instancesDir = path.join(runtime.supportRoot, 'instances');
|
|
342
|
+
const instanceDir = path.join(instancesDir, input.instanceId);
|
|
343
|
+
if (fs.existsSync(instanceDir)) {
|
|
344
|
+
return loadMaterializedFlowInstance(instanceDir, input.instanceId, validateAIDataDefinition);
|
|
345
|
+
}
|
|
346
|
+
if (!input.bundleDir || !input.source) {
|
|
347
|
+
if (input.bundleDir || input.source) throw new Error('bundleDir and source identity must be provided together.');
|
|
348
|
+
throw new Error('bundleDir and source identity are required for first admission.');
|
|
349
|
+
}
|
|
350
|
+
return materializeFlowInstance({
|
|
351
|
+
definitionDir: input.bundleDir,
|
|
352
|
+
instancesDir,
|
|
353
|
+
instanceId: input.instanceId,
|
|
354
|
+
source: input.source,
|
|
355
|
+
validateDefinition: validateAIDataDefinition,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function validateAIDataDefinition(definitionDir: string): void {
|
|
360
|
+
loadAIDataBinding(definitionDir);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function loadAIDataBinding(definitionDir: string) {
|
|
364
|
+
const assembled = assembleDefinitionStepProfileDirectory(definitionDir, 'AIDataWorkflow');
|
|
365
|
+
if (assembled.diagnostics.length > 0) {
|
|
366
|
+
throw new Error(`AIDataWorkflow StepSpace is invalid: ${assembled.diagnostics.map((item) => item.code).join(', ')}`);
|
|
367
|
+
}
|
|
368
|
+
const sources = Object.fromEntries((assembled.sources as readonly ({ readonly ref: string; readonly content: string } | { readonly name: string; readonly content: string })[])
|
|
369
|
+
.map((source) => ['name' in source ? source.name : source.ref, source.content]));
|
|
370
|
+
const loaded = loadAIDataWorkflowSources(sources, { baseUri: definitionDir });
|
|
371
|
+
if (!loaded.binding || loaded.diagnostics.length > 0) {
|
|
372
|
+
throw new Error(`AIDataWorkflow definition is invalid: ${loaded.diagnostics.map((item) => item.code).join(', ')}`);
|
|
373
|
+
}
|
|
374
|
+
return assembled.forest
|
|
375
|
+
? { ...loaded.binding, definition: { ...loaded.binding.definition, definitionStepForest: assembled.forest } }
|
|
376
|
+
: loaded.binding;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function durableGraph(graph: AIDataWorkflowRunGraph): AIDataWorkflowRunGraph {
|
|
380
|
+
const { baseUri: _baseUri, ...definition } = graph.binding.definition;
|
|
381
|
+
return {
|
|
382
|
+
...graph,
|
|
383
|
+
binding: { ...graph.binding, definition },
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function nodeStatuses(graph: AIDataWorkflowRunGraph) {
|
|
388
|
+
return Object.fromEntries(
|
|
389
|
+
graph.declarationOrder.map((nodeId) => [nodeId, {
|
|
390
|
+
status: graph.nodes[nodeId].status,
|
|
391
|
+
generation: graph.nodes[nodeId].generation,
|
|
392
|
+
}]),
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function graphCheckpointStatus(
|
|
397
|
+
graph: AIDataWorkflowRunGraph,
|
|
398
|
+
): 'Pending' | 'Running' | 'Succeeded' | 'Failed' {
|
|
399
|
+
const nodes = Object.values(graph.nodes).filter((node) => node.status !== 'Removed');
|
|
400
|
+
if (nodes.some((node) => node.status === 'Failed')) return 'Failed';
|
|
401
|
+
if (nodes.some((node) => node.status === 'Running')) return 'Running';
|
|
402
|
+
if (nodes.length > 0 && nodes.every((node) => node.status === 'Succeeded' || node.status === 'Reused')) {
|
|
403
|
+
return 'Succeeded';
|
|
404
|
+
}
|
|
405
|
+
return 'Pending';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function graphCheckpointOutput(
|
|
409
|
+
graph: AIDataWorkflowRunGraph,
|
|
410
|
+
status: ReturnType<typeof graphCheckpointStatus>,
|
|
411
|
+
): FlowClosedValue {
|
|
412
|
+
if (status !== 'Succeeded') return null;
|
|
413
|
+
const returnNode = graph.declarationOrder
|
|
414
|
+
.map((nodeId) => graph.nodes[nodeId])
|
|
415
|
+
.find((node) => node?.tag === 'ReturnNode');
|
|
416
|
+
if (!returnNode?.result || returnNode.result.output === undefined) {
|
|
417
|
+
throw new Error(`AI Data Workflow ${graph.runId} terminal RunGraph has no Return result.`);
|
|
418
|
+
}
|
|
419
|
+
return normalizeFlowClosedValue(returnNode.result.output, 'aiData.execution.output');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function checkpointStatus(checkpoint: AIDataWorkflowRunCheckpoint): unknown {
|
|
423
|
+
return checkpoint.controllerSidecars.status;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function assertStartMatchesCheckpoint(
|
|
427
|
+
checkpoint: AIDataWorkflowRunCheckpoint,
|
|
428
|
+
input: StartMaterializedAIDataWorkflowInput,
|
|
429
|
+
config: StartMaterializedAIDataWorkflowConfig,
|
|
430
|
+
): void {
|
|
431
|
+
const normalizedInput = normalizeFlowClosedObject(input.input, 'aiData.start.input');
|
|
432
|
+
const normalizedConfig = normalizeFlowClosedValue(config.checkpointConfig, 'aiData.start.config');
|
|
433
|
+
if (JSON.stringify(normalizedInput) !== JSON.stringify(checkpoint.input)
|
|
434
|
+
|| JSON.stringify(normalizedConfig) !== JSON.stringify(checkpoint.config)
|
|
435
|
+
|| config.platformDefaultPolicy !== checkpoint.profile.runGraph.platformDefaultPolicy) {
|
|
436
|
+
throw new Error(`AI Data Workflow run ${input.runId} is already bound to different start facts.`);
|
|
437
|
+
}
|
|
438
|
+
if (checkpointStatus(checkpoint) === 'Pending') {
|
|
439
|
+
const normalizedAI = normalizeAIWorkflowProfileDurableState(config.durableAI, 'aiData.start.ai');
|
|
440
|
+
if ((config.initialState !== undefined
|
|
441
|
+
&& JSON.stringify(normalizeFlowClosedValue(config.initialState, 'aiData.start.state')) !== JSON.stringify(checkpoint.state))
|
|
442
|
+
|| JSON.stringify(normalizedAI) !== JSON.stringify(checkpoint.profile.ai)) {
|
|
443
|
+
throw new Error(`AI Data Workflow run ${input.runId} has different pending initialization facts.`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,13 +7,18 @@ import type {
|
|
|
7
7
|
AIDataWorkflowNodeTag,
|
|
8
8
|
AIDataWorkflowReuseCandidate,
|
|
9
9
|
AIDataWorkflowRunGraph,
|
|
10
|
+
AIDataWorkflowRunCheckpoint,
|
|
10
11
|
AIDataWorkflowRunNode,
|
|
11
12
|
AIDataWorkflowRunNodeStatus,
|
|
12
13
|
AIDataWorkflowSemanticInput,
|
|
13
14
|
} from 'ai-data-workflow-contract';
|
|
14
15
|
import type {
|
|
15
16
|
AIDataWorkflowDefinitionBinding,
|
|
17
|
+
AIDataWorkflowRunProfileState,
|
|
16
18
|
AIDataWorkflowRuntime,
|
|
19
|
+
AIAgentHostEffectPort,
|
|
20
|
+
FrozenAIAgentTaskBinding,
|
|
21
|
+
AIWorkflowAuthoredEffects,
|
|
17
22
|
AIWorkflowDefinitionRef,
|
|
18
23
|
AIWorkflowEffectProvider,
|
|
19
24
|
AIWorkflowEffectiveReusePolicy,
|
|
@@ -24,8 +29,16 @@ import type {
|
|
|
24
29
|
AIWorkflowRunState,
|
|
25
30
|
AIWorkflowRunStatus,
|
|
26
31
|
AIWorkflowRuntimeContext,
|
|
32
|
+
AIWorkflowProfileDurableState,
|
|
27
33
|
AIWorkflowSemanticFingerprint,
|
|
28
|
-
|
|
34
|
+
AIWorkflowStateProjection,
|
|
35
|
+
FlowClosedObject,
|
|
36
|
+
FlowClosedValue,
|
|
37
|
+
FlowDefinitionArtifactIdentity,
|
|
38
|
+
FlowRunCheckpointCommit,
|
|
39
|
+
FlowRunCheckpointKey,
|
|
40
|
+
FlowRunCheckpointRuntime,
|
|
41
|
+
FlowRunProfileState,
|
|
29
42
|
} from 'ai-workflow-contract';
|
|
30
43
|
import type {
|
|
31
44
|
EagerDataFlowAuthoringPlan,
|
|
@@ -38,9 +51,76 @@ import type {
|
|
|
38
51
|
import { loadEagerDataFlowProfileSources } from 'eager-data-flow-logic';
|
|
39
52
|
import {
|
|
40
53
|
createAIWorkflowRuntimeContext,
|
|
54
|
+
commitFlowRunCheckpoint,
|
|
41
55
|
defineAIDataWorkflow,
|
|
56
|
+
loadFlowRunCheckpoint,
|
|
42
57
|
resolveAIWorkflowReusePolicy,
|
|
58
|
+
bindAIAgentProcessors,
|
|
43
59
|
} from 'ai-workflow-logic';
|
|
60
|
+
import { assertFrozenAIAgentTaskBinding } from 'ai-workflow-logic/run-freeze';
|
|
61
|
+
|
|
62
|
+
export interface CreateAIDataWorkflowCheckpointInput {
|
|
63
|
+
readonly instanceId: string;
|
|
64
|
+
readonly definition: FlowDefinitionArtifactIdentity;
|
|
65
|
+
readonly graph: AIDataWorkflowRunGraph;
|
|
66
|
+
readonly input: FlowClosedValue;
|
|
67
|
+
readonly config: FlowClosedValue;
|
|
68
|
+
readonly state: FlowClosedValue;
|
|
69
|
+
readonly output: FlowClosedValue;
|
|
70
|
+
readonly controllerSidecars: FlowClosedObject;
|
|
71
|
+
readonly nodeSidecars: FlowClosedObject;
|
|
72
|
+
readonly ai: AIWorkflowProfileDurableState;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Creates the first complete Data checkpoint as one runtime-owned transition. */
|
|
76
|
+
export function createAIDataWorkflowCheckpoint(
|
|
77
|
+
runtime: FlowRunCheckpointRuntime,
|
|
78
|
+
input: CreateAIDataWorkflowCheckpointInput,
|
|
79
|
+
): Promise<AIDataWorkflowRunCheckpoint> {
|
|
80
|
+
return commitAIDataWorkflowCheckpoint(runtime, {
|
|
81
|
+
expectedVersion: null,
|
|
82
|
+
checkpoint: {
|
|
83
|
+
schemaVersion: 'depa.flow-run-checkpoint/v1',
|
|
84
|
+
instanceId: input.instanceId,
|
|
85
|
+
runId: input.graph.runId,
|
|
86
|
+
definition: input.definition,
|
|
87
|
+
version: 0,
|
|
88
|
+
input: input.input,
|
|
89
|
+
config: input.config,
|
|
90
|
+
state: input.state,
|
|
91
|
+
output: input.output,
|
|
92
|
+
controllerSidecars: input.controllerSidecars,
|
|
93
|
+
nodeSidecars: input.nodeSidecars,
|
|
94
|
+
profile: { kind: 'AIDataWorkflow', runGraph: input.graph, ai: input.ai },
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function commitAIDataWorkflowCheckpoint(
|
|
100
|
+
runtime: FlowRunCheckpointRuntime,
|
|
101
|
+
input: FlowRunCheckpointCommit<AIDataWorkflowRunProfileState<AIDataWorkflowRunGraph>>,
|
|
102
|
+
): Promise<AIDataWorkflowRunCheckpoint> {
|
|
103
|
+
const checkpoint = await commitFlowRunCheckpoint(
|
|
104
|
+
runtime,
|
|
105
|
+
input as unknown as FlowRunCheckpointCommit<FlowRunProfileState>,
|
|
106
|
+
);
|
|
107
|
+
if (checkpoint.profile.kind !== 'AIDataWorkflow') {
|
|
108
|
+
throw new Error(`Flow run ${checkpoint.runId} is not an AI Data Workflow checkpoint.`);
|
|
109
|
+
}
|
|
110
|
+
return checkpoint as unknown as AIDataWorkflowRunCheckpoint;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function loadAIDataWorkflowCheckpoint(
|
|
114
|
+
runtime: FlowRunCheckpointRuntime,
|
|
115
|
+
input: FlowRunCheckpointKey,
|
|
116
|
+
): Promise<AIDataWorkflowRunCheckpoint | undefined> {
|
|
117
|
+
const checkpoint = await loadFlowRunCheckpoint(runtime, input);
|
|
118
|
+
if (checkpoint === undefined) return undefined;
|
|
119
|
+
if (checkpoint.profile.kind !== 'AIDataWorkflow') {
|
|
120
|
+
throw new Error(`Flow run ${input.runId} is not an AI Data Workflow checkpoint.`);
|
|
121
|
+
}
|
|
122
|
+
return checkpoint as unknown as AIDataWorkflowRunCheckpoint;
|
|
123
|
+
}
|
|
44
124
|
|
|
45
125
|
export class AIDataWorkflowRunGraphError extends Error {
|
|
46
126
|
constructor(message: string) {
|
|
@@ -51,14 +131,14 @@ export class AIDataWorkflowRunGraphError extends Error {
|
|
|
51
131
|
|
|
52
132
|
export interface CreateAIDataWorkflowRuntimeOptions<
|
|
53
133
|
Effects extends AIWorkflowEffectProvider,
|
|
54
|
-
Store extends
|
|
134
|
+
Store extends AIWorkflowStateProjection,
|
|
55
135
|
> {
|
|
56
136
|
readonly ai: AIWorkflowRuntimeContext<Effects, Store>;
|
|
57
137
|
}
|
|
58
138
|
|
|
59
139
|
export function createAIDataWorkflowRuntime<
|
|
60
140
|
Effects extends AIWorkflowEffectProvider,
|
|
61
|
-
Store extends
|
|
141
|
+
Store extends AIWorkflowStateProjection,
|
|
62
142
|
>(
|
|
63
143
|
options: CreateAIDataWorkflowRuntimeOptions<Effects, Store>,
|
|
64
144
|
): AIDataWorkflowRuntime<Effects, Store> {
|
|
@@ -67,6 +147,42 @@ export function createAIDataWorkflowRuntime<
|
|
|
67
147
|
};
|
|
68
148
|
}
|
|
69
149
|
|
|
150
|
+
export interface CreateAIDataWorkflowAgentNodeRuntimeBinderOptions {
|
|
151
|
+
readonly flowInstanceId: string;
|
|
152
|
+
readonly runId: string;
|
|
153
|
+
readonly checkpointRuntime: FlowRunCheckpointRuntime;
|
|
154
|
+
readonly effects: AIAgentHostEffectPort;
|
|
155
|
+
readonly workflowRef: `resource://${string}`;
|
|
156
|
+
readonly taskProofs: Readonly<Record<string, FrozenAIAgentTaskBinding>>;
|
|
157
|
+
readonly generationForNode: (nodeId: string) => number;
|
|
158
|
+
readonly metadata?: FlowClosedObject;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Composition seam for ordinary TransformNode and SinkNode Processors. */
|
|
162
|
+
export function createAIDataWorkflowAgentNodeRuntimeBinder(
|
|
163
|
+
options: CreateAIDataWorkflowAgentNodeRuntimeBinderOptions,
|
|
164
|
+
): (runtime: AIDataWorkflowRuntime<any, AIWorkflowStateProjection>, identity: { readonly nodeId: string }) => AIDataWorkflowRuntime<AIWorkflowAuthoredEffects, AIWorkflowStateProjection> {
|
|
165
|
+
return (runtime, identity) => {
|
|
166
|
+
const generation = options.generationForNode(identity.nodeId);
|
|
167
|
+
const proof = options.taskProofs[identity.nodeId];
|
|
168
|
+
if (!proof) throw new Error(`AI_AGENT_TASK_PROOF_REQUIRED: no frozen proof for node ${identity.nodeId}.`);
|
|
169
|
+
const taskBinding = assertFrozenAIAgentTaskBinding(proof).task;
|
|
170
|
+
const effects = bindAIAgentProcessors({
|
|
171
|
+
checkpointRuntime: options.checkpointRuntime,
|
|
172
|
+
checkpointKey: { instanceId: options.flowInstanceId, runId: options.runId },
|
|
173
|
+
nodeId: identity.nodeId,
|
|
174
|
+
invocationKey: `${identity.nodeId}#${generation}`,
|
|
175
|
+
generation,
|
|
176
|
+
workflowKind: 'AIDataWorkflow',
|
|
177
|
+
workflowRef: options.workflowRef,
|
|
178
|
+
taskBinding,
|
|
179
|
+
effects: options.effects,
|
|
180
|
+
...(options.metadata === undefined ? {} : { metadata: options.metadata }),
|
|
181
|
+
});
|
|
182
|
+
return Object.freeze({ ai: Object.freeze({ ...runtime.ai, effects }) });
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
70
186
|
export function defineEagerBackedAIDataWorkflow(
|
|
71
187
|
definition: AIDataWorkflowDefinitionBinding['definition'],
|
|
72
188
|
): AIDataWorkflowDefinitionBinding {
|
|
@@ -296,6 +412,73 @@ function assertKnownDependencies(
|
|
|
296
412
|
`AI Data Workflow node ${nodeId} depends on unknown node ${dependency}`,
|
|
297
413
|
);
|
|
298
414
|
}
|
|
415
|
+
if (nodes[dependency]?.status === 'Removed') {
|
|
416
|
+
throw new AIDataWorkflowRunGraphError(
|
|
417
|
+
`AI Data Workflow node ${nodeId} cannot depend on removed node ${dependency}`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function assertRunGraphInputReferences(
|
|
424
|
+
nodes: Readonly<Record<string, AIDataWorkflowRunNode>>,
|
|
425
|
+
): void {
|
|
426
|
+
for (const node of Object.values(nodes)) {
|
|
427
|
+
for (const [inputName, input] of Object.entries(node.inputs)) {
|
|
428
|
+
if (inputName.length === 0 || inputName !== inputName.trim()) {
|
|
429
|
+
throw new AIDataWorkflowRunGraphError(`AI Data Workflow node ${node.id} has an invalid input identity`);
|
|
430
|
+
}
|
|
431
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) continue;
|
|
432
|
+
if (!('ref' in input) && !('nodeId' in input) && !('port' in input)) continue;
|
|
433
|
+
const reference = input as { readonly ref?: unknown; readonly nodeId?: unknown; readonly port?: unknown };
|
|
434
|
+
if (typeof reference.nodeId !== 'string' || typeof reference.port !== 'string'
|
|
435
|
+
|| reference.ref !== `flow-port://#${reference.nodeId}/${reference.port}`) {
|
|
436
|
+
throw new AIDataWorkflowRunGraphError(
|
|
437
|
+
`AI Data Workflow node ${node.id} input ${inputName} has an invalid flow-port reference`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
const source = nodes[reference.nodeId];
|
|
441
|
+
if (!source || source.status === 'Removed' || !source.outputs.includes(reference.port)) {
|
|
442
|
+
throw new AIDataWorkflowRunGraphError(
|
|
443
|
+
`AI Data Workflow node ${node.id} input ${inputName} references an undeclared source output`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
if (!node.dependsOn.includes(reference.nodeId)) {
|
|
447
|
+
throw new AIDataWorkflowRunGraphError(
|
|
448
|
+
`AI Data Workflow node ${node.id} input ${inputName} is not projected by dependsOn`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function assertReusePolicyAuthority(
|
|
456
|
+
reusePolicy: AIWorkflowEffectiveReusePolicy,
|
|
457
|
+
nodeType: AIWorkflowEffectiveReusePolicy['nodeType'],
|
|
458
|
+
config: Readonly<Record<string, unknown>>,
|
|
459
|
+
platformDefaultPolicy: AIWorkflowReusePolicy | undefined,
|
|
460
|
+
nodeId: string,
|
|
461
|
+
): void {
|
|
462
|
+
if (reusePolicy.nodeType !== nodeType) {
|
|
463
|
+
throw new AIDataWorkflowRunGraphError(
|
|
464
|
+
`AI Data Workflow node ${nodeId} reuse policy nodeType must equal ${nodeType}`,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
const explicitValue = config.reuse_policy ?? config.reusePolicy;
|
|
468
|
+
const explicitPolicy = explicitValue === 'semantic-hash' || explicitValue === 'never'
|
|
469
|
+
? explicitValue
|
|
470
|
+
: undefined;
|
|
471
|
+
const expected = explicitPolicy
|
|
472
|
+
? { policy: explicitPolicy, source: 'node', nodeType }
|
|
473
|
+
: nodeType === 'manual'
|
|
474
|
+
? { policy: 'never', source: 'node-type-default', nodeType }
|
|
475
|
+
: platformDefaultPolicy
|
|
476
|
+
? { policy: platformDefaultPolicy, source: 'platform-default', nodeType }
|
|
477
|
+
: { policy: 'semantic-hash', source: 'node-type-default', nodeType };
|
|
478
|
+
if (reusePolicy.policy !== expected.policy || reusePolicy.source !== expected.source) {
|
|
479
|
+
throw new AIDataWorkflowRunGraphError(
|
|
480
|
+
`AI Data Workflow node ${nodeId} reuse policy must match its declared config and platform authority`,
|
|
481
|
+
);
|
|
299
482
|
}
|
|
300
483
|
}
|
|
301
484
|
|
|
@@ -319,6 +502,7 @@ export function createAIDataWorkflowRunGraph(
|
|
|
319
502
|
explicitPolicy: configReusePolicy(config),
|
|
320
503
|
platformDefaultPolicy,
|
|
321
504
|
});
|
|
505
|
+
assertReusePolicyAuthority(reusePolicy, nodeType, config, platformDefaultPolicy, planNode.id);
|
|
322
506
|
const runNode: AIDataWorkflowRunNode = {
|
|
323
507
|
id: planNode.id,
|
|
324
508
|
tag: planNode.tag,
|
|
@@ -341,15 +525,18 @@ export function createAIDataWorkflowRunGraph(
|
|
|
341
525
|
assertKnownDependencies(nodes, node.id, node.dependsOn);
|
|
342
526
|
}
|
|
343
527
|
|
|
344
|
-
|
|
528
|
+
const graph = {
|
|
345
529
|
runId,
|
|
346
530
|
binding,
|
|
531
|
+
...(platformDefaultPolicy === undefined ? {} : { platformDefaultPolicy }),
|
|
347
532
|
currentGeneration: 0,
|
|
348
533
|
nodes: recomputeDependents(nodes, declarationOrder),
|
|
349
534
|
declarationOrder,
|
|
350
535
|
patchHistory: [],
|
|
351
536
|
invalidations: [],
|
|
352
537
|
};
|
|
538
|
+
assertRunGraphInputReferences(graph.nodes);
|
|
539
|
+
return graph;
|
|
353
540
|
}
|
|
354
541
|
|
|
355
542
|
function makePatchNode(
|
|
@@ -360,6 +547,12 @@ function makePatchNode(
|
|
|
360
547
|
const tag = spec.tag ?? GENERATED_NODE_TAG;
|
|
361
548
|
const config = cloneRecord(spec.config);
|
|
362
549
|
const nodeType = nodeTypeForPatchNode({ ...spec, tag, config });
|
|
550
|
+
const reusePolicy = spec.reusePolicy ?? resolveAIWorkflowReusePolicy({
|
|
551
|
+
nodeType,
|
|
552
|
+
explicitPolicy: configReusePolicy(config),
|
|
553
|
+
platformDefaultPolicy,
|
|
554
|
+
});
|
|
555
|
+
assertReusePolicyAuthority(reusePolicy, nodeType, config, platformDefaultPolicy, spec.id);
|
|
363
556
|
return {
|
|
364
557
|
id: spec.id,
|
|
365
558
|
tag,
|
|
@@ -372,11 +565,7 @@ function makePatchNode(
|
|
|
372
565
|
config,
|
|
373
566
|
dependsOn: uniqueStrings(spec.dependsOn ?? []),
|
|
374
567
|
dependents: [],
|
|
375
|
-
reusePolicy
|
|
376
|
-
nodeType,
|
|
377
|
-
explicitPolicy: configReusePolicy(config),
|
|
378
|
-
platformDefaultPolicy,
|
|
379
|
-
}),
|
|
568
|
+
reusePolicy,
|
|
380
569
|
};
|
|
381
570
|
}
|
|
382
571
|
|
|
@@ -412,13 +601,18 @@ function applyInvalidations(
|
|
|
412
601
|
let nextNodes: Record<string, AIDataWorkflowRunNode> = Object.fromEntries(
|
|
413
602
|
Object.entries(nodes).map(([nodeId, node]) => [nodeId, cloneNode(node)]),
|
|
414
603
|
);
|
|
415
|
-
|
|
604
|
+
let invalidations = existingInvalidations.map(cloneInvalidation);
|
|
416
605
|
const emitted = new Set<string>();
|
|
417
606
|
|
|
418
607
|
for (const source of sources) {
|
|
419
608
|
for (const nodeId of collectTransitive(nextNodes, source.nodeId, source.includeSource)) {
|
|
420
609
|
const node = nextNodes[nodeId];
|
|
421
610
|
if (!node || node.status === 'Removed') continue;
|
|
611
|
+
if (generation < node.generation) {
|
|
612
|
+
throw new AIDataWorkflowRunGraphError(
|
|
613
|
+
`AI Data Workflow invalidation generation ${generation} precedes node ${nodeId}@${node.generation}`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
422
616
|
const key = `${source.nodeId}:${nodeId}:${generation}`;
|
|
423
617
|
if (emitted.has(key)) continue;
|
|
424
618
|
emitted.add(key);
|
|
@@ -429,14 +623,20 @@ function applyInvalidations(
|
|
|
429
623
|
sourceNodeId: source.nodeId,
|
|
430
624
|
...(atMs === undefined ? {} : { atMs }),
|
|
431
625
|
};
|
|
626
|
+
invalidations = invalidations.filter((fact) => fact.nodeId !== nodeId);
|
|
432
627
|
invalidations.push(invalidation);
|
|
628
|
+
const {
|
|
629
|
+
result: _staleResult,
|
|
630
|
+
semanticFingerprint: _staleFingerprint,
|
|
631
|
+
...currentFacts
|
|
632
|
+
} = node;
|
|
433
633
|
nextNodes = {
|
|
434
634
|
...nextNodes,
|
|
435
635
|
[nodeId]: {
|
|
436
|
-
...
|
|
636
|
+
...currentFacts,
|
|
437
637
|
status: 'Invalidated',
|
|
438
638
|
generation,
|
|
439
|
-
invalidatedBy: [
|
|
639
|
+
invalidatedBy: [invalidation],
|
|
440
640
|
},
|
|
441
641
|
};
|
|
442
642
|
}
|
|
@@ -454,6 +654,15 @@ export function invalidateAIDataWorkflowNode(
|
|
|
454
654
|
requireNode(graph.nodes, nodeId);
|
|
455
655
|
const generation = options.generation ?? graph.currentGeneration;
|
|
456
656
|
const sourceNodeId = options.sourceNodeId ?? nodeId;
|
|
657
|
+
requireNode(graph.nodes, sourceNodeId);
|
|
658
|
+
if (!Number.isSafeInteger(generation) || generation < 0) {
|
|
659
|
+
throw new AIDataWorkflowRunGraphError('AI Data Workflow invalidation generation must be a non-negative safe integer');
|
|
660
|
+
}
|
|
661
|
+
if (generation < graph.currentGeneration) {
|
|
662
|
+
throw new AIDataWorkflowRunGraphError(
|
|
663
|
+
`AI Data Workflow invalidation generation ${generation} precedes current generation ${graph.currentGeneration}`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
457
666
|
const cloned = cloneGraph(graph);
|
|
458
667
|
const { nodes, invalidations } = applyInvalidations(
|
|
459
668
|
cloned.nodes,
|
|
@@ -462,6 +671,7 @@ export function invalidateAIDataWorkflowNode(
|
|
|
462
671
|
generation,
|
|
463
672
|
options.atMs,
|
|
464
673
|
);
|
|
674
|
+
if (invalidations.length === cloned.invalidations.length) return cloned;
|
|
465
675
|
return {
|
|
466
676
|
...cloned,
|
|
467
677
|
currentGeneration: Math.max(cloned.currentGeneration, generation),
|
|
@@ -477,8 +687,26 @@ export function applyAIDataWorkflowGraphPatch(
|
|
|
477
687
|
if (graph.patchHistory.some((record) => record.patchId === patch.patchId)) {
|
|
478
688
|
throw new AIDataWorkflowRunGraphError(`AI Data Workflow patch ${patch.patchId} already applied`);
|
|
479
689
|
}
|
|
690
|
+
const patchTargets = new Set<string>();
|
|
691
|
+
for (const operation of patch.operations) {
|
|
692
|
+
const target = operation.op === 'add-node' ? operation.node.id : operation.nodeId;
|
|
693
|
+
if (patchTargets.has(target)) {
|
|
694
|
+
throw new AIDataWorkflowRunGraphError(
|
|
695
|
+
`AI Data Workflow patch ${patch.patchId} targets node ${target} more than once`,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
patchTargets.add(target);
|
|
699
|
+
}
|
|
480
700
|
|
|
481
|
-
const generation = graph.currentGeneration + 1;
|
|
701
|
+
const generation = patch.generation ?? graph.currentGeneration + 1;
|
|
702
|
+
if (!Number.isSafeInteger(generation) || generation < 1) {
|
|
703
|
+
throw new AIDataWorkflowRunGraphError('AI Data Workflow patch generation must be a positive safe integer');
|
|
704
|
+
}
|
|
705
|
+
if (generation < graph.currentGeneration) {
|
|
706
|
+
throw new AIDataWorkflowRunGraphError(
|
|
707
|
+
`AI Data Workflow patch generation ${generation} precedes current generation ${graph.currentGeneration}`,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
482
710
|
const atMs = patch.atMs ?? Date.now();
|
|
483
711
|
let declarationOrder = [...graph.declarationOrder];
|
|
484
712
|
let nodes = Object.fromEntries(
|
|
@@ -499,6 +727,7 @@ export function applyAIDataWorkflowGraphPatch(
|
|
|
499
727
|
declarationIndex: operation.node.declarationIndex ?? declarationOrder.length,
|
|
500
728
|
},
|
|
501
729
|
generation,
|
|
730
|
+
graph.platformDefaultPolicy,
|
|
502
731
|
);
|
|
503
732
|
assertKnownDependencies(nodes, node.id, node.dependsOn);
|
|
504
733
|
nodes = { ...nodes, [node.id]: node };
|
|
@@ -508,6 +737,11 @@ export function applyAIDataWorkflowGraphPatch(
|
|
|
508
737
|
|
|
509
738
|
if (operation.op === 'update-node') {
|
|
510
739
|
const existing = requireNode(nodes, operation.nodeId);
|
|
740
|
+
if (generation < existing.generation) {
|
|
741
|
+
throw new AIDataWorkflowRunGraphError(
|
|
742
|
+
`AI Data Workflow patch generation ${generation} precedes node ${existing.id}@${existing.generation}`,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
511
745
|
if (existing.status === 'Removed') {
|
|
512
746
|
throw new AIDataWorkflowRunGraphError(
|
|
513
747
|
`AI Data Workflow node ${operation.nodeId} has been removed`,
|
|
@@ -520,13 +754,15 @@ export function applyAIDataWorkflowGraphPatch(
|
|
|
520
754
|
: existing.dependsOn;
|
|
521
755
|
assertKnownDependencies(nodes, existing.id, dependsOn);
|
|
522
756
|
const reusePolicy = operation.changes.reusePolicy ?? (
|
|
523
|
-
operation.changes.config
|
|
757
|
+
operation.changes.config || operation.changes.nodeType
|
|
524
758
|
? resolveAIWorkflowReusePolicy({
|
|
525
759
|
nodeType,
|
|
526
760
|
explicitPolicy: configReusePolicy(config),
|
|
761
|
+
platformDefaultPolicy: graph.platformDefaultPolicy,
|
|
527
762
|
})
|
|
528
763
|
: existing.reusePolicy
|
|
529
764
|
);
|
|
765
|
+
assertReusePolicyAuthority(reusePolicy, nodeType, config, graph.platformDefaultPolicy, existing.id);
|
|
530
766
|
nodes = {
|
|
531
767
|
...nodes,
|
|
532
768
|
[existing.id]: {
|
|
@@ -551,10 +787,24 @@ export function applyAIDataWorkflowGraphPatch(
|
|
|
551
787
|
}
|
|
552
788
|
|
|
553
789
|
const existing = requireNode(nodes, operation.nodeId);
|
|
790
|
+
if (existing.status === 'Removed') {
|
|
791
|
+
throw new AIDataWorkflowRunGraphError(`AI Data Workflow node ${operation.nodeId} has already been removed`);
|
|
792
|
+
}
|
|
793
|
+
if (generation < existing.generation) {
|
|
794
|
+
throw new AIDataWorkflowRunGraphError(
|
|
795
|
+
`AI Data Workflow patch generation ${generation} precedes node ${existing.id}@${existing.generation}`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
const {
|
|
799
|
+
result: _staleResult,
|
|
800
|
+
semanticFingerprint: _staleFingerprint,
|
|
801
|
+
invalidatedBy: _activeInvalidation,
|
|
802
|
+
...currentFacts
|
|
803
|
+
} = existing;
|
|
554
804
|
nodes = {
|
|
555
805
|
...nodes,
|
|
556
806
|
[existing.id]: {
|
|
557
|
-
...
|
|
807
|
+
...currentFacts,
|
|
558
808
|
generation,
|
|
559
809
|
status: 'Removed',
|
|
560
810
|
},
|
|
@@ -569,13 +819,13 @@ export function applyAIDataWorkflowGraphPatch(
|
|
|
569
819
|
nodes = recomputeDependents(nodes, declarationOrder);
|
|
570
820
|
const invalidated = applyInvalidations(
|
|
571
821
|
nodes,
|
|
572
|
-
graph.invalidations,
|
|
822
|
+
graph.invalidations.filter((fact) => nodes[fact.nodeId]?.status !== 'Removed'),
|
|
573
823
|
invalidationSources,
|
|
574
824
|
generation,
|
|
575
825
|
atMs,
|
|
576
826
|
);
|
|
577
827
|
|
|
578
|
-
|
|
828
|
+
const nextGraph = {
|
|
579
829
|
...graph,
|
|
580
830
|
currentGeneration: generation,
|
|
581
831
|
nodes: invalidated.nodes,
|
|
@@ -592,6 +842,11 @@ export function applyAIDataWorkflowGraphPatch(
|
|
|
592
842
|
],
|
|
593
843
|
invalidations: invalidated.invalidations,
|
|
594
844
|
};
|
|
845
|
+
for (const node of Object.values(nextGraph.nodes)) {
|
|
846
|
+
if (node.status !== 'Removed') assertKnownDependencies(nextGraph.nodes, node.id, node.dependsOn);
|
|
847
|
+
}
|
|
848
|
+
assertRunGraphInputReferences(nextGraph.nodes);
|
|
849
|
+
return nextGraph;
|
|
595
850
|
}
|
|
596
851
|
|
|
597
852
|
function stableSemanticValue(value: unknown): string {
|
|
@@ -608,10 +863,14 @@ function stableSemanticValue(value: unknown): string {
|
|
|
608
863
|
|
|
609
864
|
const entries = Object.entries(value as Record<string, unknown>)
|
|
610
865
|
.filter(([, item]) => typeof item !== 'function' && typeof item !== 'symbol')
|
|
611
|
-
.sort(([left], [right]) => left
|
|
866
|
+
.sort(([left], [right]) => compareCodeUnits(left, right));
|
|
612
867
|
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableSemanticValue(item)}`).join(',')}}`;
|
|
613
868
|
}
|
|
614
869
|
|
|
870
|
+
function compareCodeUnits(left: string, right: string): number {
|
|
871
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
872
|
+
}
|
|
873
|
+
|
|
615
874
|
function fnv1a64(value: string): string {
|
|
616
875
|
let hash = 0xcbf29ce484222325n;
|
|
617
876
|
const prime = 0x100000001b3n;
|
|
@@ -695,10 +954,29 @@ export function recordAIDataWorkflowNodeResult(
|
|
|
695
954
|
result: AIWorkflowNodeResult,
|
|
696
955
|
): AIDataWorkflowRunGraph {
|
|
697
956
|
const node = requireNode(graph.nodes, nodeId);
|
|
957
|
+
if (result.nodeId !== nodeId) {
|
|
958
|
+
throw new AIDataWorkflowRunGraphError(`AI Data Workflow result identity ${result.nodeId} does not match node ${nodeId}`);
|
|
959
|
+
}
|
|
960
|
+
if (result.generation !== node.generation) {
|
|
961
|
+
throw new AIDataWorkflowRunGraphError(
|
|
962
|
+
`AI Data Workflow result generation ${result.generation} does not match node ${nodeId}@${node.generation}`,
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
if (node.result
|
|
966
|
+
&& (node.result.status === 'Succeeded' || node.result.status === 'Failed' || node.result.status === 'Reused')) {
|
|
967
|
+
if (JSON.stringify(node.result) === JSON.stringify(result)) return cloneGraph(graph);
|
|
968
|
+
throw new AIDataWorkflowRunGraphError(
|
|
969
|
+
`AI Data Workflow terminal result ${nodeId}@${node.generation} is immutable`,
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
const {
|
|
973
|
+
invalidatedBy: _acceptedInvalidation,
|
|
974
|
+
...nodeFacts
|
|
975
|
+
} = node;
|
|
698
976
|
const nodes = {
|
|
699
977
|
...graph.nodes,
|
|
700
978
|
[nodeId]: {
|
|
701
|
-
...
|
|
979
|
+
...nodeFacts,
|
|
702
980
|
generation: result.generation,
|
|
703
981
|
status: toRunNodeStatus(result.status),
|
|
704
982
|
result: cloneNodeResult(result),
|
|
@@ -709,6 +987,7 @@ export function recordAIDataWorkflowNodeResult(
|
|
|
709
987
|
...graph,
|
|
710
988
|
currentGeneration: Math.max(graph.currentGeneration, result.generation),
|
|
711
989
|
nodes: recomputeDependents(nodes, graph.declarationOrder),
|
|
990
|
+
invalidations: graph.invalidations.filter((fact) => fact.nodeId !== nodeId),
|
|
712
991
|
};
|
|
713
992
|
}
|
|
714
993
|
|