ai-workflow-logic 0.1.9 → 0.1.11

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.
@@ -121,8 +121,8 @@ function reserveName(ai: AIWorkflowProfileDurableState, receipt: AIAgentInvocati
121
121
  }
122
122
 
123
123
  function resolveSelector(ai: AIWorkflowProfileDurableState, selector: AIAgentSelector, config: AIAgentEffectConfig): AIAgentInstanceReference {
124
- const instanceId = 'byInstanceId' in selector ? selector.byInstanceId : ai.instanceIdByName[selector.byInstanceName];
125
- if (instanceId === undefined) fail('AI_AGENT_INSTANCE_NAME_NOT_FOUND', 'selector.byInstanceName', 'Named Agent instance does not exist.');
124
+ const instanceId = 'byId' in selector ? selector.byId : ai.instanceIdByName[selector.byName];
125
+ if (instanceId === undefined) fail('AI_AGENT_INSTANCE_NAME_NOT_FOUND', 'selector.byName', 'Named Agent instance does not exist.');
126
126
  const instance = ai.instancesById[instanceId];
127
127
  if (!instance) fail('AI_AGENT_INSTANCE_ID_NOT_FOUND', 'selector', 'Agent instance does not exist.');
128
128
  if (instance.instanceName !== undefined && ai.instanceIdByName[instance.instanceName] !== instance.instanceId) fail('AI_AGENT_INSTANCE_INDEX_CONFLICT', 'checkpoint.profile.ai', 'Agent instance indexes are not reciprocal.');
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  AIAgentDefinitionProjection,
3
+ AIAgentMessagePrefixItemProjection,
3
4
  AIAgentMessageProjection,
4
5
  AIAgentMessageRole,
5
6
  AIWorkflowEffectPolicyProjection,
@@ -27,6 +28,8 @@ import { AIWorkflowResourceProjectionError } from './errors.js';
27
28
 
28
29
  const AGENT_SUBDOMAINS = Object.freeze([
29
30
  'Messages',
31
+ 'MessagePrefix',
32
+ 'ContextPipeline',
30
33
  'InputSchemaRef',
31
34
  'OutputSchemaRef',
32
35
  'ToolRefs',
@@ -144,12 +147,44 @@ function projectAgentDefinition(
144
147
  diagnostics: AIWorkflowResourceProjectionDiagnostic[],
145
148
  ): AIAgentDefinitionProjection | undefined {
146
149
  validateAllowedSubdomains(resource, AGENT_SUBDOMAINS, diagnostics);
147
- const messagesNode = requiredSubdomain(resource, 'Messages', diagnostics);
150
+ const messagesNode = exactOptionalSubdomain(resource, 'Messages', diagnostics);
151
+ const messagePrefixNode = exactOptionalSubdomain(resource, 'MessagePrefix', diagnostics);
152
+ if (messagesNode && messagePrefixNode) {
153
+ diagnostics.push(diagnostic(
154
+ 'AI_AGENT_MESSAGE_PREFIX_CONFLICT',
155
+ resource.documentUri,
156
+ 'AIAgentDefinition must declare exactly one of legacy Messages or MessagePrefix.',
157
+ ));
158
+ } else if (!messagesNode && !messagePrefixNode) {
159
+ diagnostics.push(diagnostic(
160
+ 'AI_AGENT_MESSAGE_PREFIX_MISSING',
161
+ resource.documentUri,
162
+ 'AIAgentDefinition must declare legacy Messages or MessagePrefix.',
163
+ ));
164
+ }
148
165
  const toolRefsNode = requiredSubdomain(resource, 'ToolRefs', diagnostics);
149
166
  const materialPortRefsNode = requiredSubdomain(resource, 'MaterialPortRefs', diagnostics);
167
+ const projectedMessagePrefix = messagePrefixNode
168
+ ? projectMessagePrefix(resource, messagePrefixNode, resources, edges, diagnostics)
169
+ : undefined;
150
170
  const messages = messagesNode
151
171
  ? projectMessages(resource, messagesNode, resources, edges, diagnostics)
152
- : [];
172
+ : projectedMessagePrefix
173
+ ? projectedMessagePrefix
174
+ .filter((item): item is Extract<AIAgentMessagePrefixItemProjection, { type: 'message' }> => item.type === 'message')
175
+ .map(({ type: _type, ...message }) => Object.freeze(message))
176
+ : [];
177
+ const messagePrefix = projectedMessagePrefix
178
+ ?? messages.map((message) => Object.freeze({ ...message, type: 'message' as const }));
179
+ const contextPipeline = projectOptionalTypedSubdomain(
180
+ resource,
181
+ 'ContextPipeline',
182
+ resources,
183
+ 'agent-context-pipeline',
184
+ edges,
185
+ diagnostics,
186
+ 'AgentContextPipeline',
187
+ );
153
188
  const inputSchema = projectOptionalSchemaSubdomain(resource, 'InputSchemaRef', resources, 'agent-input-schema', edges, diagnostics);
154
189
  const outputSchema = projectOptionalSchemaSubdomain(resource, 'OutputSchemaRef', resources, 'agent-output-schema', edges, diagnostics);
155
190
  const tools = toolRefsNode
@@ -160,12 +195,14 @@ function projectAgentDefinition(
160
195
  ? projectTypedRefList(resource, materialPortRefsNode, 'MaterialPortRef', resources, 'agent-material-port', edges, diagnostics, 'MaterialPort')
161
196
  : [];
162
197
 
163
- if (!messagesNode || !toolRefsNode || !materialPortRefsNode) return undefined;
198
+ if ((!messagesNode && !messagePrefixNode) || (messagesNode && messagePrefixNode) || !toolRefsNode || !materialPortRefsNode) return undefined;
164
199
  return Object.freeze({
165
200
  kind: 'AIAgentDefinition',
166
201
  fqn: resource.fqn ?? resource.resourceId,
167
202
  resource,
203
+ messagePrefix: Object.freeze(messagePrefix),
168
204
  messages: Object.freeze(messages),
205
+ ...(contextPipeline === undefined ? {} : { contextPipeline }),
169
206
  ...(inputSchema === undefined ? {} : { inputSchema }),
170
207
  ...(outputSchema === undefined ? {} : { outputSchema }),
171
208
  tools: Object.freeze(tools),
@@ -174,6 +211,67 @@ function projectAgentDefinition(
174
211
  });
175
212
  }
176
213
 
214
+ function exactOptionalSubdomain(
215
+ resource: ResourceRecord,
216
+ name: string,
217
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
218
+ ): ResourceNode | undefined {
219
+ const value = resource.node.subdomains[name];
220
+ if (!value) return undefined;
221
+ if (value.tag !== name) {
222
+ diagnostics.push(diagnostic(
223
+ 'AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH',
224
+ `${resource.documentUri}#${name}`,
225
+ `Subdomain ${name} must contain a ${name} node, got '${value.tag}'.`,
226
+ ));
227
+ return undefined;
228
+ }
229
+ return value;
230
+ }
231
+
232
+ function projectMessagePrefix(
233
+ resource: ResourceRecord,
234
+ prefixNode: ResourceNode,
235
+ resources: ReadonlyMap<string, ResourceRecord>,
236
+ edges: ResourceDependencyEdge[],
237
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
238
+ ): AIAgentMessagePrefixItemProjection[] {
239
+ const output: AIAgentMessagePrefixItemProjection[] = [];
240
+ const ids = new Set<string>();
241
+ prefixNode.body.forEach((value, index) => {
242
+ const location = `${resource.documentUri}#MessagePrefix:${index}`;
243
+ if (!isResourceNode(value) || (value.tag !== 'Message' && value.tag !== 'MessageSource')) {
244
+ diagnostics.push(diagnostic(
245
+ 'AI_AGENT_MESSAGE_PREFIX_NODE_INVALID',
246
+ location,
247
+ 'MessagePrefix may contain only Message or MessageSource nodes.',
248
+ ));
249
+ return;
250
+ }
251
+ const id = canonicalString(value.resourceId);
252
+ if (!id) {
253
+ diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_MISSING', location, `${value.tag} must declare an explicit #id.`));
254
+ return;
255
+ }
256
+ if (ids.has(id)) {
257
+ diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_DUPLICATE', location, `MessagePrefix id '${id}' is declared more than once.`));
258
+ return;
259
+ }
260
+ ids.add(id);
261
+ if (value.tag === 'Message') {
262
+ const message = projectMessageNode(resource, value, resources, edges, diagnostics, location);
263
+ if (message) output.push(Object.freeze({ ...message, type: 'message' }));
264
+ return;
265
+ }
266
+ validateAllowedNodeSubdomains(value, [], location, diagnostics);
267
+ const source = projectTypedValues(value.properties.kind, value.properties.ref, resources, location, diagnostics, 'AgentMessageSource');
268
+ if (!source) return;
269
+ edges.push(edge(resource.resourceId, source.resource.resourceId, 'agent-message-source'));
270
+ output.push(Object.freeze({ type: 'message-source', id, source }));
271
+ });
272
+ return output;
273
+ }
274
+
177
275
  function projectMessages(
178
276
  resource: ResourceRecord,
179
277
  messagesNode: ResourceNode,
@@ -189,39 +287,45 @@ function projectMessages(
189
287
  diagnostics.push(diagnostic('AI_AGENT_MESSAGE_NODE_INVALID', location, 'Messages may contain only Message nodes.'));
190
288
  return;
191
289
  }
192
- validateAllowedNodeSubdomains(value, ['SchemaRef'], location, diagnostics);
193
290
  const id = canonicalString(value.resourceId);
194
- const role = value.properties.role;
195
- if (!id) {
196
- diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_MISSING', location, 'Message must declare an explicit #id.'));
197
- } else if (ids.has(id)) {
291
+ if (id && ids.has(id)) {
198
292
  diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_DUPLICATE', location, `Message id '${id}' is declared more than once.`));
199
- } else {
200
- ids.add(id);
293
+ return;
201
294
  }
202
- if (!isMessageRole(role)) {
203
- diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ROLE_INVALID', location, 'Message role must be system, developer, user, or assistant.'));
295
+ if (id) {
296
+ ids.add(id);
204
297
  }
205
- const prompt = projectTypedValues(
206
- value.properties.promptKind,
207
- value.properties.promptRef,
208
- resources,
209
- location,
210
- diagnostics,
211
- );
212
- if (prompt) edges.push(edge(resource.resourceId, prompt.resource.resourceId, 'agent-message-prompt'));
213
- const schemaNode = value.subdomains.SchemaRef;
214
- const schema = schemaNode?.tag === 'SchemaRef'
215
- ? projectMessageSchemaRefNode(schemaNode, resources, `${location}#SchemaRef`, diagnostics)
216
- : undefined;
217
- if (schemaNode && schemaNode.tag !== 'SchemaRef') diagnostics.push(diagnostic('AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH', `${location}#SchemaRef`, `Subdomain SchemaRef must contain a SchemaRef node, got '${schemaNode.tag}'.`));
218
- if (schema) edges.push(edge(resource.resourceId, schema.resource.resourceId, 'agent-message-schema'));
219
- if (!id || !isMessageRole(role) || !prompt) return;
220
- output.push(Object.freeze({ id, role, prompt, ...(schema === undefined ? {} : { schema }) }));
298
+ const message = projectMessageNode(resource, value, resources, edges, diagnostics, location);
299
+ if (message) output.push(message);
221
300
  });
222
301
  return output;
223
302
  }
224
303
 
304
+ function projectMessageNode(
305
+ resource: ResourceRecord,
306
+ value: ResourceNode,
307
+ resources: ReadonlyMap<string, ResourceRecord>,
308
+ edges: ResourceDependencyEdge[],
309
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
310
+ location: string,
311
+ ): AIAgentMessageProjection | undefined {
312
+ validateAllowedNodeSubdomains(value, ['SchemaRef'], location, diagnostics);
313
+ const id = canonicalString(value.resourceId);
314
+ const role = value.properties.role;
315
+ if (!id) diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_MISSING', location, 'Message must declare an explicit #id.'));
316
+ if (!isMessageRole(role)) diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ROLE_INVALID', location, 'Message role must be system, developer, user, or assistant.'));
317
+ const prompt = projectTypedValues(value.properties.promptKind, value.properties.promptRef, resources, location, diagnostics);
318
+ if (prompt) edges.push(edge(resource.resourceId, prompt.resource.resourceId, 'agent-message-prompt'));
319
+ const schemaNode = value.subdomains.SchemaRef;
320
+ const schema = schemaNode?.tag === 'SchemaRef'
321
+ ? projectMessageSchemaRefNode(schemaNode, resources, `${location}#SchemaRef`, diagnostics)
322
+ : undefined;
323
+ if (schemaNode && schemaNode.tag !== 'SchemaRef') diagnostics.push(diagnostic('AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH', `${location}#SchemaRef`, `Subdomain SchemaRef must contain a SchemaRef node, got '${schemaNode.tag}'.`));
324
+ if (schema) edges.push(edge(resource.resourceId, schema.resource.resourceId, 'agent-message-schema'));
325
+ if (!id || !isMessageRole(role) || !prompt) return undefined;
326
+ return Object.freeze({ id, role, prompt, ...(schema === undefined ? {} : { schema }) });
327
+ }
328
+
225
329
  function projectTypedRefList(
226
330
  owner: ResourceRecord,
227
331
  list: ResourceNode,
@@ -265,6 +369,7 @@ function projectOptionalTypedSubdomain(
265
369
  relation: AIWorkflowAgentResourceRelation,
266
370
  edges: ResourceDependencyEdge[],
267
371
  diagnostics: AIWorkflowResourceProjectionDiagnostic[],
372
+ expectedKind?: string,
268
373
  ): AIWorkflowTypedResourceRef | undefined {
269
374
  const node = owner.node.subdomains[name];
270
375
  if (!node) return undefined;
@@ -272,7 +377,7 @@ function projectOptionalTypedSubdomain(
272
377
  diagnostics.push(diagnostic('AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH', `${owner.documentUri}#${name}`, `Subdomain ${name} must contain a ${name} node, got '${node.tag}'.`));
273
378
  return undefined;
274
379
  }
275
- const typedRef = projectTypedRefNode(node, resources, `${owner.documentUri}#${name}`, diagnostics);
380
+ const typedRef = projectTypedRefNode(node, resources, `${owner.documentUri}#${name}`, diagnostics, expectedKind);
276
381
  if (typedRef) edges.push(edge(owner.resourceId, typedRef.resource.resourceId, relation));
277
382
  return typedRef;
278
383
  }
@@ -115,13 +115,13 @@ function optionalStringArray(value: unknown, location: string): readonly string[
115
115
  }
116
116
 
117
117
  export function normalizeAIAgentSelector(value: unknown, location = 'selector'): AIAgentSelector {
118
- const input = record(value, [], ['byInstanceId', 'byInstanceName'], location);
119
- const hasId = Object.prototype.hasOwnProperty.call(input, 'byInstanceId');
120
- const hasName = Object.prototype.hasOwnProperty.call(input, 'byInstanceName');
118
+ const input = record(value, [], ['byId', 'byName'], location);
119
+ const hasId = Object.prototype.hasOwnProperty.call(input, 'byId');
120
+ const hasName = Object.prototype.hasOwnProperty.call(input, 'byName');
121
121
  if (hasId === hasName) fail('AI_AGENT_SELECTOR_EXACTLY_ONE', location, 'Selector must contain exactly one supported key.');
122
122
  return Object.freeze(hasId
123
- ? { byInstanceId: identity(input.byInstanceId, `${location}.byInstanceId`) }
124
- : { byInstanceName: identity(input.byInstanceName, `${location}.byInstanceName`) });
123
+ ? { byId: identity(input.byId, `${location}.byId`) }
124
+ : { byName: identity(input.byName, `${location}.byName`) });
125
125
  }
126
126
 
127
127
  export function normalizeAIAgentEffectConfig(value: unknown, location = 'config'): AIAgentEffectConfig {
@@ -399,7 +399,9 @@ function validateAIDataProfileTransition(
399
399
  throw new FlowRunCheckpointTransitionError(`AI Data patch operation ${operation.op} is unsupported.`);
400
400
  }
401
401
  for (const [field, expected] of Object.entries(operation.changes)) {
402
- if (!sameClosedValue(candidate[field as keyof typeof candidate], expected)) {
402
+ const actual = (candidate as Readonly<Record<string, unknown>>)[field];
403
+ if (field === 'subflow' && expected === null && actual === undefined) continue;
404
+ if (!sameClosedValue(actual, expected)) {
403
405
  throw new FlowRunCheckpointTransitionError(`AI Data update-node ${nodeId} does not project declared ${field}.`);
404
406
  }
405
407
  }
package/src/index.ts CHANGED
@@ -36,6 +36,21 @@ import { AIWorkflowFoundationError, AIWorkflowResourceProjectionError } from './
36
36
 
37
37
  export { AIWorkflowFoundationError, AIWorkflowResourceProjectionError } from './errors.js';
38
38
  export { projectAIWorkflowAgentResources } from './agent-resources.js';
39
+ export {
40
+ AIAgentDefinitionSelectionError,
41
+ admitAIAgentDefinitionSelection,
42
+ freezeSelectedAIAgentTaskBinding,
43
+ normalizeAIAgentTaskRequirement,
44
+ projectAIAgentDefinitionCandidates,
45
+ reconcileAuthoredAIAgentDefinition,
46
+ } from './agent-definition-selection.js';
47
+ export type {
48
+ AIAgentDefinitionSelectionErrorCode,
49
+ AdmitAIAgentDefinitionSelectionInput,
50
+ FreezeSelectedAIAgentTaskBindingInput,
51
+ ProjectAIAgentDefinitionCandidatesInput,
52
+ ReconcileAuthoredAIAgentDefinitionInput,
53
+ } from './agent-definition-selection.js';
39
54
  export {
40
55
  FlowRunContractValidationError,
41
56
  normalizeAIWorkflowProfileDurableState,
@@ -755,7 +755,10 @@ function normalizeAIDataRunGraph(value: unknown, runId: string, location: string
755
755
  'id', 'tag', 'nodeType', 'generation', 'status', 'declarationIndex', 'inputs',
756
756
  'outputs', 'config', 'dependsOn', 'dependents', 'reusePolicy',
757
757
  ],
758
- ['semanticFingerprint', 'result', 'invalidatedBy'],
758
+ [
759
+ 'subflow', 'childInvocation', 'childFreezeReceipt', 'childTerminalReceipt',
760
+ 'semanticFingerprint', 'result', 'invalidatedBy',
761
+ ],
759
762
  nodeLocation,
760
763
  );
761
764
  const storedId = canonicalIdentity(dataField(node, 'id', `${nodeLocation}.id`), `${nodeLocation}.id`);
@@ -778,6 +781,46 @@ function normalizeAIDataRunGraph(value: unknown, runId: string, location: string
778
781
  normalizeAIDataInputs(dataField(node, 'inputs', `${nodeLocation}.inputs`), `${nodeLocation}.inputs`);
779
782
  uniqueIdentityArray(dataField(node, 'outputs', `${nodeLocation}.outputs`), `${nodeLocation}.outputs`);
780
783
  const config = plainRecord(dataField(node, 'config', `${nodeLocation}.config`), `${nodeLocation}.config`);
784
+ const tag = String(dataField(node, 'tag', `${nodeLocation}.tag`));
785
+ const hasSubflow = Object.prototype.hasOwnProperty.call(node, 'subflow');
786
+ if (tag === 'SubFlowNode' && !hasSubflow) {
787
+ invalid('AI_DATA_SUBFLOW_BINDING_MISSING', `${nodeLocation}.subflow`, 'SubFlowNode requires an exact durable child binding.');
788
+ }
789
+ if (tag !== 'SubFlowNode' && hasSubflow) {
790
+ invalid('AI_DATA_SUBFLOW_BINDING_INVALID', `${nodeLocation}.subflow`, 'Only SubFlowNode may carry a child binding.');
791
+ }
792
+ if (hasSubflow) normalizeAIDataSubflowBinding(dataField(node, 'subflow', `${nodeLocation}.subflow`), `${nodeLocation}.subflow`);
793
+ const hasChildInvocation = Object.prototype.hasOwnProperty.call(node, 'childInvocation');
794
+ const hasChildFreeze = Object.prototype.hasOwnProperty.call(node, 'childFreezeReceipt');
795
+ const hasChildTerminal = Object.prototype.hasOwnProperty.call(node, 'childTerminalReceipt');
796
+ if ((hasChildInvocation || hasChildFreeze || hasChildTerminal) && tag !== 'SubFlowNode') {
797
+ invalid('AI_DATA_CHILD_INVOCATION_NODE_INVALID', nodeLocation, 'Only SubFlowNode may carry child invocation facts.');
798
+ }
799
+ if ((hasChildFreeze || hasChildTerminal) && !hasChildInvocation) {
800
+ invalid('AI_DATA_CHILD_INVOCATION_IDENTITY_MISSING', nodeLocation, 'Child receipts require a reserved invocation identity.');
801
+ }
802
+ if (hasChildInvocation) {
803
+ const identity = normalizeAIDataChildInvocationIdentity(
804
+ dataField(node, 'childInvocation', `${nodeLocation}.childInvocation`),
805
+ graphRunId,
806
+ nodeId,
807
+ generation,
808
+ dataField(node, 'subflow', `${nodeLocation}.subflow`),
809
+ `${nodeLocation}.childInvocation`,
810
+ );
811
+ if (hasChildFreeze) normalizeAIDataChildFreezeReceipt(
812
+ dataField(node, 'childFreezeReceipt', `${nodeLocation}.childFreezeReceipt`),
813
+ identity,
814
+ `${nodeLocation}.childFreezeReceipt`,
815
+ );
816
+ if (hasChildTerminal) normalizeAIDataChildTerminalReceipt(
817
+ dataField(node, 'childTerminalReceipt', `${nodeLocation}.childTerminalReceipt`),
818
+ identity,
819
+ node,
820
+ String(status),
821
+ `${nodeLocation}.childTerminalReceipt`,
822
+ );
823
+ }
781
824
  const dependsOn = uniqueIdentityArray(dataField(node, 'dependsOn', `${nodeLocation}.dependsOn`), `${nodeLocation}.dependsOn`);
782
825
  const dependents = uniqueIdentityArray(dataField(node, 'dependents', `${nodeLocation}.dependents`), `${nodeLocation}.dependents`);
783
826
  normalizeReusePolicy(
@@ -811,6 +854,17 @@ function normalizeAIDataRunGraph(value: unknown, runId: string, location: string
811
854
  `${nodeLocation}.result`,
812
855
  );
813
856
  normalizeAIDataResultOutput(resultValue, node, String(status), bindingDefinition, `${nodeLocation}.result`);
857
+ if (hasChildTerminal) {
858
+ const childReceipt = plainRecord(
859
+ dataField(node, 'childTerminalReceipt', `${nodeLocation}.childTerminalReceipt`),
860
+ `${nodeLocation}.childTerminalReceipt`,
861
+ );
862
+ if (String(status) === 'Succeeded'
863
+ && JSON.stringify(dataField(childReceipt, 'output', `${nodeLocation}.childTerminalReceipt.output`))
864
+ !== JSON.stringify(dataField(plainRecord(resultValue, `${nodeLocation}.result`), 'output', `${nodeLocation}.result.output`))) {
865
+ invalid('AI_DATA_CHILD_PARENT_OUTPUT_MISMATCH', `${nodeLocation}.result.output`, 'Parent result must equal the accepted child output.');
866
+ }
867
+ }
814
868
  }
815
869
  const invalidationFacts: string[] = [];
816
870
  if (Object.prototype.hasOwnProperty.call(node, 'invalidatedBy')) {
@@ -988,12 +1042,176 @@ function normalizeInitialAIDataNodeProjection(
988
1042
  ? dataField(definitionNode, 'config', `${location}.bindingNode.config`)
989
1043
  : {},
990
1044
  dependsOn: eagerDefinitionDependencies(definition, definitionNode, nodeId, location),
1045
+ ...(dataField(definitionNode, 'tag', `${location}.bindingNode.tag`) === 'SubFlowNode'
1046
+ ? {
1047
+ subflow: {
1048
+ kind: 'declared',
1049
+ flowRef: dataField(definitionNode, 'flow', `${location}.bindingNode.flow`),
1050
+ },
1051
+ }
1052
+ : {}),
991
1053
  };
992
1054
  for (const field of ['tag', 'declarationIndex', 'inputs', 'outputs', 'config', 'dependsOn'] as const) {
993
1055
  if (JSON.stringify(dataField(node, field, `${location}.${field}`)) !== JSON.stringify(projected[field])) {
994
1056
  invalid('AI_DATA_NODE_PROJECTION_MISMATCH', `${location}.${field}`, `Initial run node ${field} differs from its binding projection.`);
995
1057
  }
996
1058
  }
1059
+ if (Object.prototype.hasOwnProperty.call(projected, 'subflow')
1060
+ && JSON.stringify(dataField(node, 'subflow', `${location}.subflow`)) !== JSON.stringify(projected.subflow)) {
1061
+ invalid('AI_DATA_NODE_PROJECTION_MISMATCH', `${location}.subflow`, 'Initial run node subflow differs from its binding projection.');
1062
+ }
1063
+ }
1064
+
1065
+ function normalizeAIDataSubflowBinding(value: unknown, location: string): void {
1066
+ const binding = plainRecord(value, location);
1067
+ const kind = dataField(binding, 'kind', `${location}.kind`);
1068
+ if (kind === 'declared') {
1069
+ exactRecord(value, ['kind', 'flowRef'], [], location);
1070
+ } else if (kind === 'autonomous') {
1071
+ const autonomous = exactRecord(
1072
+ value,
1073
+ ['kind', 'schemaVersion', 'capabilityId', 'catalogDigest', 'definitionRef', 'flowRef', 'contractDigest'],
1074
+ [],
1075
+ location,
1076
+ );
1077
+ if (dataField(autonomous, 'schemaVersion', `${location}.schemaVersion`) !== 'depa.ai-data-subflow-binding/v1') {
1078
+ invalid('AI_DATA_SUBFLOW_BINDING_VERSION_INVALID', `${location}.schemaVersion`, 'Unsupported autonomous child binding version.');
1079
+ }
1080
+ const definitionRef = canonicalIdentity(dataField(autonomous, 'definitionRef', `${location}.definitionRef`), `${location}.definitionRef`);
1081
+ if (!definitionRef.startsWith('resource://')) {
1082
+ invalid('AI_DATA_SUBFLOW_DEFINITION_REF_INVALID', `${location}.definitionRef`, 'Child definition must use resource:// identity.');
1083
+ }
1084
+ for (const field of ['capabilityId', 'catalogDigest', 'contractDigest'] as const) {
1085
+ canonicalIdentity(dataField(autonomous, field, `${location}.${field}`), `${location}.${field}`);
1086
+ }
1087
+ } else {
1088
+ invalid('AI_DATA_SUBFLOW_BINDING_KIND_INVALID', `${location}.kind`, 'Child binding kind must be declared or autonomous.');
1089
+ }
1090
+ const flowRef = canonicalIdentity(dataField(binding, 'flowRef', `${location}.flowRef`), `${location}.flowRef`);
1091
+ if (!flowRef.startsWith('eager-data-flow://')) {
1092
+ invalid('AI_DATA_SUBFLOW_FLOW_REF_INVALID', `${location}.flowRef`, 'Child flow must use eager-data-flow:// identity.');
1093
+ }
1094
+ }
1095
+
1096
+ interface NormalizedAIDataChildInvocationIdentity {
1097
+ readonly invocationKey: string;
1098
+ readonly childInstanceId: string;
1099
+ readonly childRunId: string;
1100
+ readonly definitionRef: string;
1101
+ readonly flowRef: string;
1102
+ readonly contractDigest: string;
1103
+ }
1104
+
1105
+ function normalizeAIDataChildInvocationIdentity(
1106
+ value: unknown,
1107
+ parentRunId: string,
1108
+ parentNodeId: string,
1109
+ parentGeneration: number,
1110
+ subflowValue: unknown,
1111
+ location: string,
1112
+ ): NormalizedAIDataChildInvocationIdentity {
1113
+ const identity = exactRecord(value, [
1114
+ 'schemaVersion', 'invocationKey', 'parentInstanceId', 'parentRunId', 'parentNodeId', 'parentGeneration',
1115
+ 'childInstanceId', 'childRunId', 'subflow',
1116
+ ], [], location);
1117
+ if (dataField(identity, 'schemaVersion', `${location}.schemaVersion`) !== 'depa.ai-data-child-invocation/v1') {
1118
+ invalid('AI_DATA_CHILD_INVOCATION_SCHEMA_INVALID', `${location}.schemaVersion`, 'Unsupported child invocation schema.');
1119
+ }
1120
+ canonicalIdentity(dataField(identity, 'parentInstanceId', `${location}.parentInstanceId`), `${location}.parentInstanceId`);
1121
+ if (canonicalIdentity(dataField(identity, 'parentRunId', `${location}.parentRunId`), `${location}.parentRunId`) !== parentRunId
1122
+ || canonicalIdentity(dataField(identity, 'parentNodeId', `${location}.parentNodeId`), `${location}.parentNodeId`) !== parentNodeId
1123
+ || nonNegativeSafeInteger(dataField(identity, 'parentGeneration', `${location}.parentGeneration`), `${location}.parentGeneration`) !== parentGeneration) {
1124
+ invalid('AI_DATA_CHILD_INVOCATION_PARENT_MISMATCH', location, 'Child invocation identity must bind the owning parent node generation.');
1125
+ }
1126
+ const subflow = dataField(identity, 'subflow', `${location}.subflow`);
1127
+ normalizeAIDataSubflowBinding(subflow, `${location}.subflow`);
1128
+ if (JSON.stringify(subflow) !== JSON.stringify(subflowValue)) {
1129
+ invalid('AI_DATA_CHILD_INVOCATION_SUBFLOW_MISMATCH', `${location}.subflow`, 'Child invocation must bind the exact node subflow.');
1130
+ }
1131
+ const normalizedSubflow = plainRecord(subflow, `${location}.subflow`);
1132
+ if (dataField(normalizedSubflow, 'kind', `${location}.subflow.kind`) !== 'autonomous') {
1133
+ invalid('AI_DATA_CHILD_INVOCATION_SUBFLOW_INVALID', `${location}.subflow`, 'Durable child invocation requires an autonomous subflow binding.');
1134
+ }
1135
+ return {
1136
+ invocationKey: canonicalIdentity(dataField(identity, 'invocationKey', `${location}.invocationKey`), `${location}.invocationKey`),
1137
+ childInstanceId: canonicalIdentity(dataField(identity, 'childInstanceId', `${location}.childInstanceId`), `${location}.childInstanceId`),
1138
+ childRunId: canonicalIdentity(dataField(identity, 'childRunId', `${location}.childRunId`), `${location}.childRunId`),
1139
+ definitionRef: canonicalIdentity(dataField(normalizedSubflow, 'definitionRef', `${location}.subflow.definitionRef`), `${location}.subflow.definitionRef`),
1140
+ flowRef: canonicalIdentity(dataField(normalizedSubflow, 'flowRef', `${location}.subflow.flowRef`), `${location}.subflow.flowRef`),
1141
+ contractDigest: canonicalIdentity(dataField(normalizedSubflow, 'contractDigest', `${location}.subflow.contractDigest`), `${location}.subflow.contractDigest`),
1142
+ };
1143
+ }
1144
+
1145
+ function assertAIDataChildReceiptIdentity(
1146
+ value: Record<string, unknown>,
1147
+ identity: NormalizedAIDataChildInvocationIdentity,
1148
+ location: string,
1149
+ ): void {
1150
+ for (const [field, expected] of Object.entries(identity)) {
1151
+ if (field === 'definitionRef' || field === 'flowRef' || field === 'contractDigest'
1152
+ || field === 'invocationKey' || field === 'childInstanceId' || field === 'childRunId') {
1153
+ if (canonicalIdentity(dataField(value, field, `${location}.${field}`), `${location}.${field}`) !== expected) {
1154
+ invalid('AI_DATA_CHILD_RECEIPT_IDENTITY_MISMATCH', `${location}.${field}`, 'Child receipt does not match the reserved invocation.');
1155
+ }
1156
+ }
1157
+ }
1158
+ }
1159
+
1160
+ function normalizeAIDataChildFreezeReceipt(
1161
+ value: unknown,
1162
+ identity: NormalizedAIDataChildInvocationIdentity,
1163
+ location: string,
1164
+ ): void {
1165
+ const receipt = exactRecord(value, [
1166
+ 'schemaVersion', 'invocationKey', 'childInstanceId', 'childRunId', 'definitionRef', 'flowRef',
1167
+ 'contractDigest', 'definitionRevision', 'definitionDigest',
1168
+ ], [], location);
1169
+ if (dataField(receipt, 'schemaVersion', `${location}.schemaVersion`) !== 'depa.ai-data-child-invocation/v1') {
1170
+ invalid('AI_DATA_CHILD_FREEZE_SCHEMA_INVALID', `${location}.schemaVersion`, 'Unsupported child freeze schema.');
1171
+ }
1172
+ assertAIDataChildReceiptIdentity(receipt, identity, location);
1173
+ canonicalIdentity(dataField(receipt, 'definitionRevision', `${location}.definitionRevision`), `${location}.definitionRevision`);
1174
+ canonicalIdentity(dataField(receipt, 'definitionDigest', `${location}.definitionDigest`), `${location}.definitionDigest`);
1175
+ }
1176
+
1177
+ function normalizeAIDataChildTerminalReceipt(
1178
+ value: unknown,
1179
+ identity: NormalizedAIDataChildInvocationIdentity,
1180
+ node: Record<string, unknown>,
1181
+ nodeStatus: string,
1182
+ location: string,
1183
+ ): void {
1184
+ const base = plainRecord(value, location);
1185
+ const terminalStatus = dataField(base, 'status', `${location}.status`);
1186
+ const receipt = exactRecord(value, [
1187
+ 'schemaVersion', 'invocationKey', 'childInstanceId', 'childRunId', 'definitionRef', 'flowRef',
1188
+ 'contractDigest', 'status', 'receiptDigest',
1189
+ ...(terminalStatus === 'Succeeded' ? ['output'] : ['failureCode', 'failureMessage']),
1190
+ ], [], location);
1191
+ if (dataField(receipt, 'schemaVersion', `${location}.schemaVersion`) !== 'depa.ai-data-child-invocation/v1') {
1192
+ invalid('AI_DATA_CHILD_TERMINAL_SCHEMA_INVALID', `${location}.schemaVersion`, 'Unsupported child terminal schema.');
1193
+ }
1194
+ if (terminalStatus !== 'Succeeded' && terminalStatus !== 'Failed') {
1195
+ invalid('AI_DATA_CHILD_TERMINAL_STATUS_INVALID', `${location}.status`, 'Child terminal receipt must be Succeeded or Failed.');
1196
+ }
1197
+ if (terminalStatus !== nodeStatus) {
1198
+ invalid('AI_DATA_CHILD_TERMINAL_PARENT_STATUS_MISMATCH', `${location}.status`, 'Child terminal status must equal its parent node status.');
1199
+ }
1200
+ assertAIDataChildReceiptIdentity(receipt, identity, location);
1201
+ canonicalIdentity(dataField(receipt, 'receiptDigest', `${location}.receiptDigest`), `${location}.receiptDigest`);
1202
+ if (terminalStatus === 'Succeeded') {
1203
+ const output = plainRecord(dataField(receipt, 'output', `${location}.output`), `${location}.output`);
1204
+ const expected = [...identityArray(
1205
+ dataField(node, 'outputs', `${location}.nodeOutputs`),
1206
+ `${location}.nodeOutputs`,
1207
+ )].sort(compareCodeUnits);
1208
+ if (JSON.stringify(stringDataKeys(output, `${location}.output`).sort(compareCodeUnits)) !== JSON.stringify(expected)) {
1209
+ invalid('AI_DATA_CHILD_TERMINAL_OUTPUT_MISMATCH', `${location}.output`, 'Child output keys must equal the parent SubFlowNode ports.');
1210
+ }
1211
+ } else {
1212
+ canonicalIdentity(dataField(receipt, 'failureCode', `${location}.failureCode`), `${location}.failureCode`);
1213
+ canonicalIdentity(dataField(receipt, 'failureMessage', `${location}.failureMessage`), `${location}.failureMessage`);
1214
+ }
997
1215
  }
998
1216
 
999
1217
  function eagerDefinitionDependencies(
@@ -1321,7 +1539,7 @@ function normalizeAIDataPatchNodeSpec(value: unknown, location: string): string
1321
1539
  const spec = exactRecord(
1322
1540
  value,
1323
1541
  ['id'],
1324
- ['tag', 'nodeType', 'declarationIndex', 'inputs', 'outputs', 'config', 'dependsOn', 'reusePolicy'],
1542
+ ['tag', 'nodeType', 'declarationIndex', 'inputs', 'outputs', 'config', 'subflow', 'dependsOn', 'reusePolicy'],
1325
1543
  location,
1326
1544
  );
1327
1545
  const nodeId = canonicalIdentity(dataField(spec, 'id', `${location}.id`), `${location}.id`);
@@ -1333,7 +1551,7 @@ function normalizeAIDataPatchNodeChanges(value: unknown, location: string): void
1333
1551
  const changes = exactRecord(
1334
1552
  value,
1335
1553
  [],
1336
- ['tag', 'nodeType', 'inputs', 'outputs', 'config', 'dependsOn', 'reusePolicy'],
1554
+ ['tag', 'nodeType', 'inputs', 'outputs', 'config', 'subflow', 'dependsOn', 'reusePolicy'],
1337
1555
  location,
1338
1556
  );
1339
1557
  normalizeAIDataPatchNodeFields(changes, location);
@@ -1349,6 +1567,10 @@ function normalizeAIDataPatchNodeFields(record: Record<string, unknown>, locatio
1349
1567
  if (Object.prototype.hasOwnProperty.call(record, field)) uniqueIdentityArray(dataField(record, field, `${location}.${field}`), `${location}.${field}`);
1350
1568
  }
1351
1569
  if (Object.prototype.hasOwnProperty.call(record, 'config')) plainRecord(dataField(record, 'config', `${location}.config`), `${location}.config`);
1570
+ if (Object.prototype.hasOwnProperty.call(record, 'subflow')) {
1571
+ const value = dataField(record, 'subflow', `${location}.subflow`);
1572
+ if (value !== null) normalizeAIDataSubflowBinding(value, `${location}.subflow`);
1573
+ }
1352
1574
  if (Object.prototype.hasOwnProperty.call(record, 'reusePolicy')) {
1353
1575
  const reuse = exactRecord(dataField(record, 'reusePolicy', `${location}.reusePolicy`), ['policy', 'source', 'nodeType'], [], `${location}.reusePolicy`);
1354
1576
  canonicalIdentity(dataField(reuse, 'nodeType', `${location}.reusePolicy.nodeType`), `${location}.reusePolicy.nodeType`);
package/src/run-freeze.ts CHANGED
@@ -329,16 +329,26 @@ function semanticResourceFingerprint(
329
329
  },
330
330
  agent: {
331
331
  resourceId: agent.resource.resourceId,
332
- messages: agent.messages.map((message) => ({
333
- id: message.id,
334
- role: message.role,
335
- prompt: [message.prompt.kind, message.prompt.resource.resourceId],
336
- schema: message.schema ? {
337
- kind: message.schema.kind,
338
- resourceId: message.schema.resource.resourceId,
339
- value: message.schema.schema,
340
- } : null,
341
- })),
332
+ messagePrefix: agent.messagePrefix.map((item) => item.type === 'message'
333
+ ? {
334
+ type: item.type,
335
+ id: item.id,
336
+ role: item.role,
337
+ prompt: [item.prompt.kind, item.prompt.resource.resourceId],
338
+ schema: item.schema ? {
339
+ kind: item.schema.kind,
340
+ resourceId: item.schema.resource.resourceId,
341
+ value: item.schema.schema,
342
+ } : null,
343
+ }
344
+ : {
345
+ type: item.type,
346
+ id: item.id,
347
+ source: [item.source.kind, item.source.resource.resourceId],
348
+ }),
349
+ contextPipeline: agent.contextPipeline
350
+ ? [agent.contextPipeline.kind, agent.contextPipeline.resource.resourceId]
351
+ : null,
342
352
  inputSchema: agent.inputSchema ? {
343
353
  kind: agent.inputSchema.kind,
344
354
  resourceId: agent.inputSchema.resource.resourceId,