ai-data-workflow-logic 0.1.5 → 0.1.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-data-workflow-logic",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "description": "EagerDataFlow-backed AI Data Workflow RunGraph helpers",
6
6
  "type": "module",
@@ -11,12 +11,13 @@
11
11
  "./filesystem": "./src/filesystem.ts"
12
12
  },
13
13
  "dependencies": {
14
- "ai-data-workflow-contract": "0.1.1",
15
- "ai-workflow-contract": "0.1.4",
16
- "ai-workflow-logic": "0.1.4",
17
- "eager-data-flow-contract": "0.1.1",
18
- "eager-data-flow-logic": "0.1.2",
19
- "work-ctrl-flow-logic": "0.1.1"
14
+ "ai-data-workflow-contract": "0.1.3",
15
+ "ai-workflow-contract": "0.1.6",
16
+ "ai-workflow-logic": "0.1.6",
17
+ "eager-data-flow-contract": "0.1.3",
18
+ "eager-data-flow-logic": "0.1.4",
19
+ "flow-step-space-logic": "0.1.1",
20
+ "work-ctrl-flow-logic": "0.1.3"
20
21
  },
21
22
  "files": [
22
23
  "src"
package/src/filesystem.ts CHANGED
@@ -20,12 +20,24 @@ import {
20
20
  createFilesystemCodeResolver,
21
21
  materializeEagerDataFlowPlan,
22
22
  } from 'eager-data-flow-logic';
23
- import { FilesystemFlowRunCheckpointStore } from 'ai-workflow-logic/checkpoint-filesystem';
24
23
  import {
24
+ assembleDefinitionStepProfileDirectory,
25
+ createFilesystemDefinitionStepProfileRuntime,
26
+ } from 'flow-step-space-logic/filesystem';
27
+ import {
28
+ createFilesystemFlowRunCheckpointRuntime,
29
+ createFilesystemFlowRunCheckpointStore,
30
+ } from 'ai-workflow-logic/checkpoint-filesystem';
31
+ import {
32
+ createInitialRunStepExtensions,
25
33
  normalizeAIWorkflowProfileDurableState,
26
34
  normalizeFlowClosedObject,
27
35
  normalizeFlowClosedValue,
36
+ type FlowRunProfileAuthorityPort,
37
+ type FlowRunProfileAuthorityRequest,
38
+ type FrozenFlowRunProfileAuthority,
28
39
  } from 'ai-workflow-logic';
40
+ import type { DefinitionStepAssemblyRuntime, DefinitionStepForest } from 'flow-step-space-logic';
29
41
  import {
30
42
  loadMaterializedFlowInstance,
31
43
  materializeFlowInstance,
@@ -45,6 +57,7 @@ export interface MaterializedAIDataWorkflowRuntime {
45
57
  readonly supportRoot: string;
46
58
  readonly workflow: AIDataWorkflowRuntime;
47
59
  readonly importModule: EagerDataFlowModuleImporter;
60
+ readonly extensionCodecs?: DefinitionStepAssemblyRuntime['extensionCodecs'];
48
61
  readonly agentEffects?: AIAgentHostEffectPort;
49
62
  readonly agentTaskProofs?: Readonly<Record<string, FrozenAIAgentTaskBinding>>;
50
63
  readonly checkpointObserver?: (event: {
@@ -81,6 +94,21 @@ export interface ContinueMaterializedAIDataWorkflowInput {
81
94
 
82
95
  export type ContinueMaterializedAIDataWorkflowConfig = Readonly<Record<never, never>>;
83
96
 
97
+ const emptyExtensionCodecs = Object.freeze({ resolve: () => undefined });
98
+
99
+ function checkpointStoreForRuntime(runtime: MaterializedAIDataWorkflowRuntime) {
100
+ const extensionCodecs = runtime.extensionCodecs ?? emptyExtensionCodecs;
101
+ return createFilesystemFlowRunCheckpointStore(
102
+ createFilesystemFlowRunCheckpointRuntime({
103
+ storeRoot: runtime.supportRoot,
104
+ extensionCodecs,
105
+ profileAuthority: createMaterializedAIDataWorkflowProfileAuthority(runtime),
106
+ }),
107
+ {},
108
+ {},
109
+ );
110
+ }
111
+
84
112
  /** Runtime-first canonical Data execution from a frozen instance. */
85
113
  export async function startMaterializedAIDataWorkflow(
86
114
  runtime: MaterializedAIDataWorkflowRuntime,
@@ -89,8 +117,8 @@ export async function startMaterializedAIDataWorkflow(
89
117
  ): Promise<MaterializedAIDataWorkflowResult> {
90
118
  requireAgentTaskBindings(runtime);
91
119
  const instance = ensureAIDataInstance(runtime, input);
92
- const binding = loadAIDataBinding(instance.definitionDir);
93
- const checkpointStore = new FilesystemFlowRunCheckpointStore({ storeRoot: runtime.supportRoot });
120
+ const binding = loadAIDataBinding(instance.definitionDir, runtime.extensionCodecs ?? emptyExtensionCodecs);
121
+ const checkpointStore = checkpointStoreForRuntime(runtime);
94
122
  const checkpointRuntime = { checkpointStore };
95
123
  const existing = await loadAIDataWorkflowCheckpoint(checkpointRuntime, input);
96
124
  if (existing) {
@@ -118,6 +146,7 @@ export async function startMaterializedAIDataWorkflow(
118
146
  controllerSidecars: { status: 'Pending' },
119
147
  nodeSidecars: nodeStatuses(graph),
120
148
  ai: config.durableAI,
149
+ stepExtensions: createInitialRunStepExtensions(profileAuthorityFromBinding(binding)),
121
150
  });
122
151
  return executeAIDataCheckpoint(runtime, binding, v0);
123
152
  }
@@ -130,10 +159,11 @@ export async function continueMaterializedAIDataWorkflow(
130
159
  ): Promise<MaterializedAIDataWorkflowResult> {
131
160
  requireAgentTaskBindings(runtime);
132
161
  const instanceDir = path.join(runtime.supportRoot, 'instances', input.instanceId);
133
- const instance = loadMaterializedFlowInstance(instanceDir, input.instanceId, validateAIDataDefinition);
134
- const binding = loadAIDataBinding(instance.definitionDir);
162
+ const codecs = runtime.extensionCodecs ?? emptyExtensionCodecs;
163
+ const instance = loadMaterializedFlowInstance(instanceDir, input.instanceId, (definitionDir) => validateAIDataDefinition(definitionDir, codecs));
164
+ const binding = loadAIDataBinding(instance.definitionDir, codecs);
135
165
  const checkpoint = await loadAIDataWorkflowCheckpoint(
136
- { checkpointStore: new FilesystemFlowRunCheckpointStore({ storeRoot: runtime.supportRoot }) },
166
+ { checkpointStore: checkpointStoreForRuntime(runtime) },
137
167
  input,
138
168
  );
139
169
  if (!checkpoint) throw new Error(`AI Data Workflow run ${input.runId} does not have a canonical checkpoint.`);
@@ -166,7 +196,7 @@ async function executeAIDataCheckpoint(
166
196
  }
167
197
  const invocationInput = checkpoint.input as Readonly<Record<string, FlowClosedValue>>;
168
198
  const checkpointRuntime = {
169
- checkpointStore: new FilesystemFlowRunCheckpointStore({ storeRoot: runtime.supportRoot }),
199
+ checkpointStore: checkpointStoreForRuntime(runtime),
170
200
  };
171
201
  let transitionTail = Promise.resolve();
172
202
  const transition = <T>(operation: () => Promise<T>): Promise<T> => {
@@ -317,10 +347,11 @@ export async function loadMaterializedAIDataWorkflowRun(
317
347
  input: Pick<StartMaterializedAIDataWorkflowInput, 'instanceId' | 'runId'>,
318
348
  ): Promise<MaterializedAIDataWorkflowResult | undefined> {
319
349
  const instanceDir = path.join(runtime.supportRoot, 'instances', input.instanceId);
320
- const instance = loadMaterializedFlowInstance(instanceDir, input.instanceId, validateAIDataDefinition);
321
- const binding = loadAIDataBinding(instance.definitionDir);
350
+ const codecs = runtime.extensionCodecs ?? emptyExtensionCodecs;
351
+ const instance = loadMaterializedFlowInstance(instanceDir, input.instanceId, (definitionDir) => validateAIDataDefinition(definitionDir, codecs));
352
+ const binding = loadAIDataBinding(instance.definitionDir, codecs);
322
353
  const checkpoint = await loadAIDataWorkflowCheckpoint(
323
- { checkpointStore: new FilesystemFlowRunCheckpointStore({ storeRoot: runtime.supportRoot }) },
354
+ { checkpointStore: checkpointStoreForRuntime(runtime) },
324
355
  input,
325
356
  );
326
357
  if (!checkpoint) return undefined;
@@ -340,7 +371,10 @@ function ensureAIDataInstance(
340
371
  const instancesDir = path.join(runtime.supportRoot, 'instances');
341
372
  const instanceDir = path.join(instancesDir, input.instanceId);
342
373
  if (fs.existsSync(instanceDir)) {
343
- return loadMaterializedFlowInstance(instanceDir, input.instanceId, validateAIDataDefinition);
374
+ return loadMaterializedFlowInstance(instanceDir, input.instanceId, (definitionDir) => validateAIDataDefinition(
375
+ definitionDir,
376
+ runtime.extensionCodecs ?? emptyExtensionCodecs,
377
+ ));
344
378
  }
345
379
  if (!input.bundleDir || !input.source) {
346
380
  if (input.bundleDir || input.source) throw new Error('bundleDir and source identity must be provided together.');
@@ -351,33 +385,108 @@ function ensureAIDataInstance(
351
385
  instancesDir,
352
386
  instanceId: input.instanceId,
353
387
  source: input.source,
354
- validateDefinition: validateAIDataDefinition,
388
+ validateDefinition: (definitionDir) => validateAIDataDefinition(
389
+ definitionDir,
390
+ runtime.extensionCodecs ?? emptyExtensionCodecs,
391
+ ),
355
392
  });
356
393
  }
357
394
 
358
- function validateAIDataDefinition(definitionDir: string): void {
359
- loadAIDataBinding(definitionDir);
395
+ function validateAIDataDefinition(
396
+ definitionDir: string,
397
+ extensionCodecs: DefinitionStepAssemblyRuntime['extensionCodecs'],
398
+ ): void {
399
+ loadAIDataBinding(definitionDir, extensionCodecs);
360
400
  }
361
401
 
362
- function loadAIDataBinding(definitionDir: string) {
363
- const sources = Object.fromEntries(
364
- fs.readdirSync(definitionDir, { withFileTypes: true })
365
- .filter((entry) => entry.isFile() && entry.name.endsWith('.xnl'))
366
- .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)
367
- .map((entry) => [entry.name, fs.readFileSync(path.join(definitionDir, entry.name), 'utf8')]),
402
+ function loadAIDataBinding(
403
+ definitionDir: string,
404
+ extensionCodecs: DefinitionStepAssemblyRuntime['extensionCodecs'],
405
+ ) {
406
+ const assembled = assembleDefinitionStepProfileDirectory(
407
+ createFilesystemDefinitionStepProfileRuntime({ rootDir: definitionDir, extensionCodecs }),
408
+ {},
409
+ { profileRoot: 'AIDataWorkflow' },
368
410
  );
411
+ if (assembled.diagnostics.length > 0) {
412
+ throw new Error(`AIDataWorkflow StepSpace is invalid: ${assembled.diagnostics.map((item) => item.code).join(', ')}`);
413
+ }
414
+ const sources = Object.fromEntries((assembled.sources as readonly ({ readonly ref: string; readonly content: string } | { readonly name: string; readonly content: string })[])
415
+ .map((source) => ['name' in source ? source.name : source.ref, source.content]));
369
416
  const loaded = loadAIDataWorkflowSources(sources, { baseUri: definitionDir });
370
417
  if (!loaded.binding || loaded.diagnostics.length > 0) {
371
418
  throw new Error(`AIDataWorkflow definition is invalid: ${loaded.diagnostics.map((item) => item.code).join(', ')}`);
372
419
  }
373
- return loaded.binding;
420
+ return assembled.forest
421
+ ? { ...loaded.binding, definition: { ...loaded.binding.definition, definitionStepForest: assembled.forest } }
422
+ : loaded.binding;
423
+ }
424
+
425
+ function profileAuthorityFromBinding(
426
+ binding: ReturnType<typeof loadAIDataBinding>,
427
+ ): FrozenFlowRunProfileAuthority {
428
+ const forest = binding.definition.definitionStepForest as DefinitionStepForest | undefined;
429
+ const definitionStepIds = forest?.stepOrder ?? binding.definition.declarationOrder;
430
+ return Object.freeze({
431
+ profileKind: 'AIDataWorkflow' as const,
432
+ definitionStepIds: Object.freeze([...definitionStepIds]),
433
+ binding: durableBinding(binding) as unknown as import('ai-workflow-contract').FlowClosedObject,
434
+ extensionsByStepId: Object.freeze(Object.fromEntries((forest?.stepOrder ?? []).flatMap((stepId) => {
435
+ const extensions = Object.fromEntries(Object.entries(forest!.steps[stepId].extensions).map(([kind, extension]) => [kind, Object.freeze({
436
+ ...(extension.schemaRef === undefined ? {} : { schemaRef: extension.schemaRef }),
437
+ initialValue: extension.value as import('ai-workflow-contract').FlowClosedValue,
438
+ })]));
439
+ return Object.keys(extensions).length === 0 ? [] : [[stepId, Object.freeze(extensions)]];
440
+ }))),
441
+ });
442
+ }
443
+
444
+ export function createMaterializedAIDataWorkflowProfileAuthority(
445
+ runtime: Pick<MaterializedAIDataWorkflowRuntime, 'supportRoot' | 'extensionCodecs'>,
446
+ ): FlowRunProfileAuthorityPort {
447
+ const extensionCodecs = runtime.extensionCodecs ?? emptyExtensionCodecs;
448
+ return Object.freeze({
449
+ load(request: FlowRunProfileAuthorityRequest) {
450
+ if (request.profileKind !== 'AIDataWorkflow') {
451
+ throw new Error(`AI Data profile authority cannot load ${request.profileKind}.`);
452
+ }
453
+ const instanceDir = path.join(runtime.supportRoot, 'instances', request.instanceId);
454
+ const instance = loadMaterializedFlowInstance(instanceDir, request.instanceId, (definitionDir) => {
455
+ validateAIDataDefinition(definitionDir, extensionCodecs);
456
+ });
457
+ if (canonicalClosed(normalizeFlowClosedValue(instance.descriptor.definition, 'profileAuthority.instanceDefinition'))
458
+ !== canonicalClosed(normalizeFlowClosedValue(request.definition, 'profileAuthority.requestDefinition'))) {
459
+ throw new Error('AI Data profile authority definition identity differs from the checkpoint.');
460
+ }
461
+ return profileAuthorityFromBinding(loadAIDataBinding(instance.definitionDir, extensionCodecs));
462
+ },
463
+ });
464
+ }
465
+
466
+ const compareCodeUnits = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0;
467
+
468
+ function canonicalClosed(value: import('ai-workflow-contract').FlowClosedValue): string {
469
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
470
+ if (Array.isArray(value)) return `[${value.map(canonicalClosed).join(',')}]`;
471
+ const record = value as import('ai-workflow-contract').FlowClosedObject;
472
+ return `{${Object.keys(record).sort(compareCodeUnits).map((key) => (
473
+ `${JSON.stringify(key)}:${canonicalClosed(record[key])}`
474
+ )).join(',')}}`;
475
+ }
476
+
477
+ function durableBinding(binding: ReturnType<typeof loadAIDataBinding>) {
478
+ const {
479
+ baseUri: _baseUri,
480
+ definitionStepForest: _definitionStepForest,
481
+ ...definition
482
+ } = binding.definition;
483
+ return { ...binding, definition };
374
484
  }
375
485
 
376
486
  function durableGraph(graph: AIDataWorkflowRunGraph): AIDataWorkflowRunGraph {
377
- const { baseUri: _baseUri, ...definition } = graph.binding.definition;
378
487
  return {
379
488
  ...graph,
380
- binding: { ...graph.binding, definition },
489
+ binding: durableBinding(graph.binding as ReturnType<typeof loadAIDataBinding>),
381
490
  };
382
491
  }
383
492
 
package/src/index.ts CHANGED
@@ -38,6 +38,7 @@ import type {
38
38
  FlowRunCheckpointCommit,
39
39
  FlowRunCheckpointKey,
40
40
  FlowRunCheckpointRuntime,
41
+ FlowRunStepExtensions,
41
42
  FlowRunProfileState,
42
43
  } from 'ai-workflow-contract';
43
44
  import type {
@@ -48,7 +49,8 @@ import type {
48
49
  EagerDataFlowValue,
49
50
  LoadEagerDataFlowOptions,
50
51
  } from 'eager-data-flow-contract';
51
- import { loadEagerDataFlowProfileSources } from 'eager-data-flow-logic';
52
+ import { loadEagerDataFlowProfileSources, loadEagerDataFlowProfileSourcesWithRuntime } from 'eager-data-flow-logic';
53
+ import { createDefinitionStepSourceRuntime, type DefinitionStepAssemblyRuntime } from 'flow-step-space-logic';
52
54
  import {
53
55
  createAIWorkflowRuntimeContext,
54
56
  commitFlowRunCheckpoint,
@@ -70,6 +72,7 @@ export interface CreateAIDataWorkflowCheckpointInput {
70
72
  readonly controllerSidecars: FlowClosedObject;
71
73
  readonly nodeSidecars: FlowClosedObject;
72
74
  readonly ai: AIWorkflowProfileDurableState;
75
+ readonly stepExtensions?: FlowRunStepExtensions;
73
76
  }
74
77
 
75
78
  /** Creates the first complete Data checkpoint as one runtime-owned transition. */
@@ -91,6 +94,7 @@ export function createAIDataWorkflowCheckpoint(
91
94
  output: input.output,
92
95
  controllerSidecars: input.controllerSidecars,
93
96
  nodeSidecars: input.nodeSidecars,
97
+ ...(input.stepExtensions === undefined ? {} : { stepExtensions: input.stepExtensions }),
94
98
  profile: { kind: 'AIDataWorkflow', runGraph: input.graph, ai: input.ai },
95
99
  },
96
100
  });
@@ -194,6 +198,34 @@ export interface AIDataWorkflowSourceLoadResult {
194
198
  readonly diagnostics: readonly EagerDataFlowDiagnostic[];
195
199
  }
196
200
 
201
+ export interface AIDataWorkflowSourceLoadRuntime {
202
+ readonly extensionCodecs: DefinitionStepAssemblyRuntime['extensionCodecs'];
203
+ }
204
+
205
+ export interface AIDataWorkflowSourceLoadInput {
206
+ readonly sources: EagerDataFlowSourceCollection;
207
+ }
208
+
209
+ export type AIDataWorkflowSourceLoadConfig = LoadEagerDataFlowOptions;
210
+
211
+ /** Processor form for profile admission with runtime-owned extension codecs. */
212
+ export function loadAIDataWorkflow(
213
+ runtime: AIDataWorkflowSourceLoadRuntime,
214
+ input: AIDataWorkflowSourceLoadInput,
215
+ config: AIDataWorkflowSourceLoadConfig = {},
216
+ ): AIDataWorkflowSourceLoadResult {
217
+ const loaded = loadEagerDataFlowProfileSourcesWithRuntime(
218
+ createDefinitionStepSourceRuntime(input.sources, runtime.extensionCodecs),
219
+ input.sources,
220
+ { ...config, profileRoot: 'AIDataWorkflow' },
221
+ );
222
+ if (!loaded.plan || loaded.diagnostics.length > 0) return { diagnostics: loaded.diagnostics };
223
+ return {
224
+ binding: defineEagerBackedAIDataWorkflow(loaded.plan),
225
+ diagnostics: loaded.diagnostics,
226
+ };
227
+ }
228
+
197
229
  export function loadAIDataWorkflowSources(
198
230
  sources: EagerDataFlowSourceCollection,
199
231
  options: LoadEagerDataFlowOptions = {},