ai-data-workflow-logic 0.1.14 → 0.2.1

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.14",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "description": "EagerDataFlow-backed AI Data Workflow RunGraph helpers",
6
6
  "type": "module",
@@ -8,18 +8,19 @@
8
8
  "exports": {
9
9
  ".": "./src/index.ts",
10
10
  "./browser": "./src/browser.ts",
11
- "./filesystem": "./src/filesystem.ts"
11
+ "./filesystem": "./src/filesystem.ts",
12
+ "./sources": "./src/source-loader.ts"
12
13
  },
13
14
  "dependencies": {
14
- "ai-data-workflow-contract": "0.1.7",
15
- "ai-workflow-contract": "0.1.9",
16
- "ai-workflow-logic": "0.1.11",
15
+ "ai-data-workflow-contract": "0.2.1",
16
+ "ai-workflow-contract": "0.2.2",
17
+ "ai-workflow-logic": "0.2.2",
17
18
  "eager-data-flow-contract": "0.1.4",
18
- "eager-data-flow-logic": "0.1.6",
19
- "flow-step-space-logic": "0.1.1",
20
- "task-manager-contract": "0.1.3",
21
- "task-manager-logic": "0.1.6",
22
- "work-ctrl-flow-logic": "0.1.4"
19
+ "eager-data-flow-logic": "0.1.8",
20
+ "flow-step-space-logic": "0.1.2",
21
+ "task-manager-contract": "0.1.4",
22
+ "task-manager-logic": "0.1.7",
23
+ "work-ctrl-flow-logic": "0.1.6"
23
24
  },
24
25
  "files": [
25
26
  "src"
package/src/after.ts ADDED
@@ -0,0 +1,78 @@
1
+ import type {AIDataWorkflowAfterPolicy, AIDataWorkflowGraphPatch, AIDataWorkflowRunGraph} from 'ai-data-workflow-contract';
2
+ import {normalizeFlowClosedObject} from 'ai-workflow-logic';
3
+ import {canonicalOwnDataDigest} from 'task-manager-contract';
4
+ import type {EagerDataFlowValue} from 'eager-data-flow-contract';
5
+ import type {FlowClosedValue} from 'ai-workflow-contract';
6
+
7
+ function requireThat(condition: unknown, code: string): asserts condition {
8
+ if (!condition) throw new Error(code);
9
+ }
10
+ /** 旧 Eager 合同的数组可写;只作脱离输入引用的纯值转换,不放宽合同。 */
11
+ function eagerValue(value: FlowClosedValue): EagerDataFlowValue {
12
+ if (Array.isArray(value)) return value.map(eagerValue);
13
+ if (value !== null && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, eagerValue(item)]));
14
+ return value;
15
+ }
16
+ function policy(value: unknown): AIDataWorkflowAfterPolicy {
17
+ const row = normalizeFlowClosedObject(value);
18
+ const keys = ['schemaVersion', 'targetNodeId', 'resultPort', 'decisionField', 'reopenValue', 'completeValue', 'maxReopens'];
19
+ requireThat(Object.keys(row).length === keys.length && keys.every(key => Object.hasOwn(row, key)), 'AI_DATA_AFTER_POLICY_FIELDS');
20
+ requireThat(row.schemaVersion === 'depa.ai-data-after/v1', 'AI_DATA_AFTER_POLICY_VERSION');
21
+ for (const key of ['targetNodeId', 'resultPort', 'decisionField', 'reopenValue', 'completeValue']) requireThat(typeof row[key] === 'string' && row[key].length > 0 && row[key].length <= 512 && row[key].trim() === row[key], 'AI_DATA_AFTER_POLICY_IDENTITY');
22
+ requireThat(typeof row.targetNodeId === 'string' && typeof row.resultPort === 'string' && typeof row.decisionField === 'string' && typeof row.reopenValue === 'string' && typeof row.completeValue === 'string', 'AI_DATA_AFTER_POLICY_IDENTITY');
23
+ requireThat(row.reopenValue !== row.completeValue && typeof row.maxReopens === 'number' && Number.isSafeInteger(row.maxReopens) && row.maxReopens >= 0 && row.maxReopens <= 5, 'AI_DATA_AFTER_POLICY_BUDGET');
24
+ return {schemaVersion: row.schemaVersion, targetNodeId: row.targetNodeId, resultPort: row.resultPort, decisionField: row.decisionField, reopenValue: row.reopenValue, completeValue: row.completeValue, maxReopens: row.maxReopens};
25
+ }
26
+
27
+ /** 纯规划;调用 owner 必须通过原 checkpoint CAS 应用返回的补丁。 */
28
+ export function planAIDataWorkflowAfterPatch(graph: AIDataWorkflowRunGraph, sourceNodeId: string): AIDataWorkflowGraphPatch | undefined {
29
+ const source = graph.nodes[sourceNodeId];
30
+ if (!source || source.config.workflowAfter === undefined || !['Succeeded', 'Reused'].includes(source.status)) return;
31
+ const declared = policy(source.config.workflowAfter);
32
+ requireThat(source.result?.generation === source.generation && source.result.status === source.status, 'AI_DATA_AFTER_SOURCE_GENERATION');
33
+ const outputs = normalizeFlowClosedObject(source.result.output);
34
+ requireThat(Object.hasOwn(outputs, declared.resultPort), 'AI_DATA_AFTER_RESULT_PORT');
35
+ const report = normalizeFlowClosedObject(outputs[declared.resultPort]);
36
+ const decision = report[declared.decisionField];
37
+ if (decision === declared.completeValue) return;
38
+ requireThat(decision === declared.reopenValue, 'AI_DATA_AFTER_DECISION_UNSETTLED');
39
+ const target = graph.nodes[declared.targetNodeId];
40
+ requireThat(target?.tag === 'TransformNode' && target.nodeType !== 'manual' && target.config.workflowReopenable === true, 'AI_DATA_AFTER_TARGET_NOT_REOPENABLE');
41
+ const seen = new Set<string>();
42
+ const visit = (nodeId: string): void => {
43
+ if (seen.has(nodeId)) return;
44
+ seen.add(nodeId);
45
+ for (const parent of graph.nodes[nodeId]?.dependsOn ?? []) visit(parent);
46
+ };
47
+ visit(sourceNodeId);
48
+ requireThat(declared.targetNodeId !== sourceNodeId && seen.has(declared.targetNodeId), 'AI_DATA_AFTER_TARGET_NOT_PREDECESSOR');
49
+ const affected = new Set<string>();
50
+ const downstream = (nodeId: string): void => {
51
+ if (affected.has(nodeId)) return;
52
+ affected.add(nodeId);
53
+ for (const node of Object.values(graph.nodes)) if (node.dependsOn.includes(nodeId)) downstream(node.id);
54
+ };
55
+ downstream(declared.targetNodeId);
56
+ for (const nodeId of affected) {
57
+ const node = graph.nodes[nodeId]!;
58
+ requireThat(node.config.humanReview !== true && node.config.workflowReopenProtected !== true, 'AI_DATA_AFTER_PROTECTED_NODE');
59
+ requireThat(node.status !== 'Running' && node.result?.status !== 'Waiting', 'AI_DATA_AFTER_UNSETTLED_NODE');
60
+ }
61
+ const prefix = 'after-reopen:' + canonicalOwnDataDigest([sourceNodeId, declared]).slice('sha256:'.length) + ':';
62
+ const previous = graph.patchHistory.filter(receipt => receipt.patchId.startsWith(prefix));
63
+ requireThat(previous.length < declared.maxReopens, 'AI_DATA_AFTER_BUDGET_EXHAUSTED');
64
+ const outputHash = canonicalOwnDataDigest(report);
65
+ const patchId = prefix + source.generation + ':' + outputHash;
66
+ requireThat(!graph.patchHistory.some(receipt => receipt.patchId === patchId), 'AI_DATA_AFTER_ALREADY_APPLIED');
67
+ const nodeResults = Object.fromEntries(Object.values(graph.nodes).filter(node => node.result).map(node => [node.id, {
68
+ nodeId: node.id, generation: node.generation, status: node.status,
69
+ ...(node.result!.output === undefined ? {} : {output: node.result!.output}),
70
+ }]));
71
+ const afterFeedback = normalizeFlowClosedObject({sourceNode: sourceNodeId, sourceGeneration: source.generation, outputHash, output: report, nodeResults});
72
+ return {
73
+ patchId,
74
+ generation: graph.currentGeneration + 1,
75
+ reason: '原生 after 准入的有界整改:' + sourceNodeId,
76
+ operations: [{op: 'update-node', nodeId: target.id, changes: {config: Object.fromEntries(Object.entries(normalizeFlowClosedObject({...target.config, afterFeedback})).map(([key, value]) => [key, eagerValue(value)]))}}],
77
+ };
78
+ }
package/src/filesystem.ts CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  normalizeAIWorkflowProfileDurableState,
41
41
  normalizeFlowClosedObject,
42
42
  normalizeFlowClosedValue,
43
+ WorkflowDelegationSuspendedError,
43
44
  type FlowRunProfileAuthorityPort,
44
45
  type FlowRunProfileAuthorityRequest,
45
46
  type FrozenFlowRunProfileAuthority,
@@ -339,7 +340,8 @@ async function executeAIDataCheckpoint(
339
340
  return commitGraph((current) => {
340
341
  const node = current.nodes[nodeId];
341
342
  if (!node) throw new Error(`AI Data Workflow node ${nodeId} does not exist in the durable graph.`);
342
- if (cause instanceof AIDataChildInvocationSuspendedError
343
+ if (findDelegationSuspension(cause)
344
+ || cause instanceof AIDataChildInvocationSuspendedError
343
345
  || cause instanceof AIDataChildInvocationTerminalFailure
344
346
  || findCheckpointObserverError(cause)) return current;
345
347
  return recordAIDataWorkflowNodeResult(current, nodeId, {
@@ -351,6 +353,7 @@ async function executeAIDataCheckpoint(
351
353
  },
352
354
  });
353
355
  } catch (error) {
356
+ if (findDelegationSuspension(error)) return { checkpoint: accepted, graph };
354
357
  const observerError = findCheckpointObserverError(error);
355
358
  if (observerError) throw observerError.cause;
356
359
  throw error;
@@ -371,6 +374,14 @@ class AIDataChildInvocationSuspendedError extends Error {
371
374
  }
372
375
  }
373
376
 
377
+ function findDelegationSuspension(value: unknown, seen = new Set<unknown>()): WorkflowDelegationSuspendedError | undefined {
378
+ if (value instanceof WorkflowDelegationSuspendedError) return value;
379
+ if (!value || typeof value !== 'object' || seen.has(value)) return undefined;
380
+ seen.add(value);
381
+ if ('cause' in value) return findDelegationSuspension(value.cause, seen);
382
+ return undefined;
383
+ }
384
+
374
385
  class AIDataChildInvocationTerminalFailure extends Error {
375
386
  constructor(nodeId: string, code: string, message: string) {
376
387
  super(`AI Data child invocation ${nodeId} failed (${code}): ${message}`);
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export {planAIDataWorkflowAfterPatch} from './after.js';
1
2
  import type {
2
3
  AIDataWorkflowGraphPatch,
3
4
  AIDataWorkflowGraphPatchNodeSpec,
@@ -20,6 +21,7 @@ import type {
20
21
  AIDataWorkflowRunProfileState,
21
22
  AIDataWorkflowRuntime,
22
23
  AIAgentHostEffectPort,
24
+ AIAgentProcessorRuntime,
23
25
  FrozenAIAgentTaskBinding,
24
26
  AIWorkflowAuthoredEffects,
25
27
  AIWorkflowDefinitionRef,
@@ -52,15 +54,15 @@ import type {
52
54
  EagerDataFlowValue,
53
55
  LoadEagerDataFlowOptions,
54
56
  } from 'eager-data-flow-contract';
55
- import { loadEagerDataFlowProfileSources, loadEagerDataFlowProfileSourcesWithRuntime } from 'eager-data-flow-logic';
56
- import { createDefinitionStepSourceRuntime, type DefinitionStepAssemblyRuntime } from 'flow-step-space-logic';
57
57
  import {
58
58
  createAIWorkflowRuntimeContext,
59
59
  commitFlowRunCheckpoint,
60
- defineAIDataWorkflow,
61
60
  loadFlowRunCheckpoint,
62
61
  resolveAIWorkflowReusePolicy,
63
62
  bindAIAgentProcessors,
63
+ bindWorkflowNodeTasks,
64
+ bindWorkflowTaskDelegation,
65
+ normalizeFlowClosedObject,
64
66
  } from 'ai-workflow-logic';
65
67
  import { assertFrozenAIAgentTaskBinding } from 'ai-workflow-logic/run-freeze';
66
68
 
@@ -178,7 +180,7 @@ export function createAIDataWorkflowAgentNodeRuntimeBinder(
178
180
  const proof = options.taskProofs[identity.nodeId];
179
181
  if (!proof) throw new Error(`AI_AGENT_TASK_PROOF_REQUIRED: no frozen proof for node ${identity.nodeId}.`);
180
182
  const taskBinding = assertFrozenAIAgentTaskBinding(proof).task;
181
- const effects = bindAIAgentProcessors({
183
+ const agentRuntime = {
182
184
  checkpointRuntime: options.checkpointRuntime,
183
185
  checkpointKey: { instanceId: options.flowInstanceId, runId: options.runId },
184
186
  nodeId: identity.nodeId,
@@ -189,66 +191,23 @@ export function createAIDataWorkflowAgentNodeRuntimeBinder(
189
191
  taskBinding,
190
192
  effects: options.effects,
191
193
  ...(options.metadata === undefined ? {} : { metadata: options.metadata }),
192
- });
193
- return Object.freeze({ ai: Object.freeze({ ...runtime.ai, effects }) });
194
+ } satisfies AIAgentProcessorRuntime;
195
+ const effects = bindAIAgentProcessors(agentRuntime);
196
+ const delegation=options.effects.delegation ? bindWorkflowTaskDelegation({
197
+ checkpointRuntime: options.checkpointRuntime,
198
+ checkpointKey: { instanceId: options.flowInstanceId, runId: options.runId },
199
+ nodeId: identity.nodeId, invocationKey: `${identity.nodeId}#${generation}`,
200
+ workflowKind: 'AIDataWorkflow', workflowRef: options.workflowRef, taskBinding,
201
+ visit: `generation:${generation}`, generation, effects: options.effects.delegation,
202
+ }) : undefined;
203
+ return Object.freeze({ ai: Object.freeze({ ...runtime.ai, effects,
204
+ tasks:bindWorkflowNodeTasks(agentRuntime,delegation),
205
+ ...(delegation?{delegation}:{}),
206
+ }) });
194
207
  };
195
208
  }
196
209
 
197
- export function defineEagerBackedAIDataWorkflow(
198
- definition: AIDataWorkflowDefinitionBinding['definition'],
199
- ): AIDataWorkflowDefinitionBinding {
200
- return defineAIDataWorkflow(definition);
201
- }
202
-
203
- export interface AIDataWorkflowSourceLoadResult {
204
- readonly binding?: AIDataWorkflowDefinitionBinding;
205
- readonly diagnostics: readonly EagerDataFlowDiagnostic[];
206
- }
207
-
208
- export interface AIDataWorkflowSourceLoadRuntime {
209
- readonly extensionCodecs: DefinitionStepAssemblyRuntime['extensionCodecs'];
210
- }
211
-
212
- export interface AIDataWorkflowSourceLoadInput {
213
- readonly sources: EagerDataFlowSourceCollection;
214
- }
215
-
216
- export type AIDataWorkflowSourceLoadConfig = LoadEagerDataFlowOptions;
217
-
218
- /** Processor form for profile admission with runtime-owned extension codecs. */
219
- export function loadAIDataWorkflow(
220
- runtime: AIDataWorkflowSourceLoadRuntime,
221
- input: AIDataWorkflowSourceLoadInput,
222
- config: AIDataWorkflowSourceLoadConfig = {},
223
- ): AIDataWorkflowSourceLoadResult {
224
- const loaded = loadEagerDataFlowProfileSourcesWithRuntime(
225
- createDefinitionStepSourceRuntime(input.sources, runtime.extensionCodecs),
226
- input.sources,
227
- { ...config, profileRoot: 'AIDataWorkflow' },
228
- );
229
- if (!loaded.plan || loaded.diagnostics.length > 0) return { diagnostics: loaded.diagnostics };
230
- return {
231
- binding: defineEagerBackedAIDataWorkflow(loaded.plan),
232
- diagnostics: loaded.diagnostics,
233
- };
234
- }
235
-
236
- export function loadAIDataWorkflowSources(
237
- sources: EagerDataFlowSourceCollection,
238
- options: LoadEagerDataFlowOptions = {},
239
- ): AIDataWorkflowSourceLoadResult {
240
- const loaded = loadEagerDataFlowProfileSources(sources, {
241
- ...options,
242
- profileRoot: 'AIDataWorkflow',
243
- });
244
- if (!loaded.plan || loaded.diagnostics.length > 0) {
245
- return { diagnostics: loaded.diagnostics };
246
- }
247
- return {
248
- binding: defineEagerBackedAIDataWorkflow(loaded.plan),
249
- diagnostics: loaded.diagnostics,
250
- };
251
- }
210
+ export * from './source-loader.js';
252
211
 
253
212
  export interface CreateAIDataWorkflowRunGraphOptions {
254
213
  readonly binding: AIDataWorkflowDefinitionBinding;
@@ -329,6 +288,7 @@ function cloneNodeResult(result: AIWorkflowNodeResult): AIWorkflowNodeResult {
329
288
  ...result,
330
289
  ...(result.semanticFingerprint ? { semanticFingerprint: cloneFingerprint(result.semanticFingerprint) } : {}),
331
290
  ...(result.reusedFrom ? { reusedFrom: { ...result.reusedFrom } } : {}),
291
+ ...(result.waitContext===undefined?{}:{waitContext:normalizeFlowClosedObject(result.waitContext)}),
332
292
  };
333
293
  }
334
294
 
@@ -1190,6 +1150,7 @@ export function recordAIDataWorkflowNodeResult(
1190
1150
  `AI Data Workflow result generation ${result.generation} does not match node ${nodeId}@${node.generation}`,
1191
1151
  );
1192
1152
  }
1153
+ if(node.result?.waitContext!==undefined&&(result.waitContext===undefined||JSON.stringify(normalizeFlowClosedObject(node.result.waitContext))!==JSON.stringify(normalizeFlowClosedObject(result.waitContext))))throw new AIDataWorkflowRunGraphError(`AI Data Workflow native wait context ${nodeId}@${node.generation} is immutable`);
1193
1154
  if (node.result
1194
1155
  && (node.result.status === 'Succeeded' || node.result.status === 'Failed' || node.result.status === 'Reused')) {
1195
1156
  if (JSON.stringify(node.result) === JSON.stringify(result)) return cloneGraph(graph);
@@ -0,0 +1,72 @@
1
+ import type { AIDataWorkflowDefinitionBinding } from "ai-workflow-contract";
2
+ import { defineAIDataWorkflow } from "ai-workflow-logic/definitions";
3
+ import type {
4
+ EagerDataFlowDiagnostic,
5
+ EagerDataFlowSourceCollection,
6
+ LoadEagerDataFlowOptions,
7
+ } from "eager-data-flow-contract";
8
+ import {
9
+ loadEagerDataFlowProfileSources,
10
+ loadEagerDataFlowProfileSourcesWithRuntime,
11
+ } from "eager-data-flow-logic/browser";
12
+ import {
13
+ createDefinitionStepSourceRuntime,
14
+ type DefinitionStepAssemblyRuntime,
15
+ } from "flow-step-space-logic/browser";
16
+
17
+ export function defineEagerBackedAIDataWorkflow(
18
+ definition: AIDataWorkflowDefinitionBinding["definition"],
19
+ ): AIDataWorkflowDefinitionBinding {
20
+ return defineAIDataWorkflow(definition);
21
+ }
22
+
23
+ export interface AIDataWorkflowSourceLoadResult {
24
+ readonly binding?: AIDataWorkflowDefinitionBinding;
25
+ readonly diagnostics: readonly EagerDataFlowDiagnostic[];
26
+ }
27
+
28
+ export interface AIDataWorkflowSourceLoadRuntime {
29
+ readonly extensionCodecs: DefinitionStepAssemblyRuntime["extensionCodecs"];
30
+ }
31
+
32
+ export interface AIDataWorkflowSourceLoadInput {
33
+ readonly sources: EagerDataFlowSourceCollection;
34
+ }
35
+
36
+ export type AIDataWorkflowSourceLoadConfig = LoadEagerDataFlowOptions;
37
+
38
+ /** Processor form for profile admission with runtime-owned extension codecs. */
39
+ export function loadAIDataWorkflow(
40
+ runtime: AIDataWorkflowSourceLoadRuntime,
41
+ input: AIDataWorkflowSourceLoadInput,
42
+ config: AIDataWorkflowSourceLoadConfig = {},
43
+ ): AIDataWorkflowSourceLoadResult {
44
+ const loaded = loadEagerDataFlowProfileSourcesWithRuntime(
45
+ createDefinitionStepSourceRuntime(input.sources, runtime.extensionCodecs),
46
+ input.sources,
47
+ { ...config, profileRoot: "AIDataWorkflow" },
48
+ );
49
+ if (!loaded.plan || loaded.diagnostics.length > 0)
50
+ return { diagnostics: loaded.diagnostics };
51
+ return {
52
+ binding: defineEagerBackedAIDataWorkflow(loaded.plan),
53
+ diagnostics: loaded.diagnostics,
54
+ };
55
+ }
56
+
57
+ export function loadAIDataWorkflowSources(
58
+ sources: EagerDataFlowSourceCollection,
59
+ options: LoadEagerDataFlowOptions = {},
60
+ ): AIDataWorkflowSourceLoadResult {
61
+ const loaded = loadEagerDataFlowProfileSources(sources, {
62
+ ...options,
63
+ profileRoot: "AIDataWorkflow",
64
+ });
65
+ if (!loaded.plan || loaded.diagnostics.length > 0) {
66
+ return { diagnostics: loaded.diagnostics };
67
+ }
68
+ return {
69
+ binding: defineEagerBackedAIDataWorkflow(loaded.plan),
70
+ diagnostics: loaded.diagnostics,
71
+ };
72
+ }