ai-workflow-logic 0.2.0 → 0.2.2

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.
@@ -92,6 +92,15 @@ export function normalizeFlowRunCheckpoint(value: unknown): AIWorkflowFlowRunChe
92
92
  ? normalizeRunStepExtensions(dataField(record, 'stepExtensions', 'checkpoint.stepExtensions'))
93
93
  : undefined;
94
94
  const profile = normalizeProfile(dataField(record, 'profile', 'checkpoint.profile'), runId);
95
+ if (profile.kind !== 'WorkCtrlFlow') {
96
+ for (const [key, record] of Object.entries(profile.ai.delegationsByKey ?? {})) {
97
+ for (const attempt of record.attempts) {
98
+ if (attempt.request.lineage.instanceId !== instanceId || attempt.request.lineage.runId !== runId) {
99
+ invalid('DELEGATION_CHECKPOINT_SCOPE_MISMATCH', `checkpoint.profile.ai.delegationsByKey.${key}`, 'Delegation lineage must belong to the canonical instance and run.');
100
+ }
101
+ }
102
+ }
103
+ }
95
104
  validateProfileProjection(profile, { input, state, output, controllerSidecars, nodeSidecars });
96
105
  return Object.freeze({
97
106
  schemaVersion,
@@ -471,7 +480,7 @@ function normalizeWorkCtrlNestedFacts(snapshot: Record<string, unknown>, locatio
471
480
  if (kind === 'Branch' && !hasBranchKey) invalid('WORK_CTRL_BRANCH_KEY_MISSING', `${factLocation}.branchKey`, 'Branch decisions require a branch key.');
472
481
  if (hasBranchKey) {
473
482
  canonicalIdentity(dataField(fact, 'branchKey', `${factLocation}.branchKey`), `${factLocation}.branchKey`);
474
- if (kind !== 'Branch') invalid('WORK_CTRL_BRANCH_KEY_INVALID', `${factLocation}.branchKey`, 'Only Branch decisions can retain a branch key.');
483
+ if (kind === 'NoMatch') invalid('WORK_CTRL_BRANCH_KEY_INVALID', `${factLocation}.branchKey`, 'NoMatch decisions cannot retain a branch key.');
475
484
  }
476
485
  }
477
486
 
@@ -501,7 +510,7 @@ function normalizeWorkCtrlNestedFacts(snapshot: Record<string, unknown>, locatio
501
510
  normalizeWorkCtrlAttempts(dataField(orchestration, 'attempts', `${orchestrationLocation}.attempts`), `${orchestrationLocation}.attempts`);
502
511
  normalizeWorkCtrlDeadlines(dataField(orchestration, 'deadlines', `${orchestrationLocation}.deadlines`), `${orchestrationLocation}.deadlines`);
503
512
  const lanes = normalizeWorkCtrlLanes(dataField(orchestration, 'lanes', `${orchestrationLocation}.lanes`), `${orchestrationLocation}.lanes`);
504
- normalizeWorkCtrlComposites(dataField(orchestration, 'composites', `${orchestrationLocation}.composites`), `${orchestrationLocation}.composites`);
513
+ normalizeWorkCtrlComposites(dataField(orchestration, 'composites', `${orchestrationLocation}.composites`), `${orchestrationLocation}.composites`, lanes);
505
514
  normalizeWorkCtrlWinners(dataField(orchestration, 'winners', `${orchestrationLocation}.winners`), lanes, `${orchestrationLocation}.winners`);
506
515
  normalizeWorkCtrlCancellations(dataField(orchestration, 'cancellations', `${orchestrationLocation}.cancellations`), `${orchestrationLocation}.cancellations`);
507
516
  }
@@ -542,7 +551,8 @@ function normalizeWorkCtrlDeadlines(value: unknown, location: string): void {
542
551
  function normalizeWorkCtrlLanes(value: unknown, location: string): Map<string, { invocationKey: string; laneId: string; status: string }> {
543
552
  const output = new Map<string, { invocationKey: string; laneId: string; status: string }>();
544
553
  for (const [key, item, itemLocation] of closedRecordEntries(value, location)) {
545
- const fact = exactRecord(item, ['invocationKey', 'laneId', 'status', 'bodyId'], ['item', 'flowState', 'output'], itemLocation);
554
+ const fact = exactRecord(item, ['invocationKey', 'laneId', 'status', 'bodyId'], ['item', 'flowState', 'output', 'started'], itemLocation);
555
+ if (Object.hasOwn(fact, 'started') && typeof dataField(fact, 'started', `${itemLocation}.started`) !== 'boolean') invalid('WORK_CTRL_LANE_STARTED_INVALID', `${itemLocation}.started`, 'Lane slot claim must be boolean.');
546
556
  const invocationKey = canonicalIdentity(dataField(fact, 'invocationKey', `${itemLocation}.invocationKey`), `${itemLocation}.invocationKey`);
547
557
  const laneId = canonicalIdentity(dataField(fact, 'laneId', `${itemLocation}.laneId`), `${itemLocation}.laneId`);
548
558
  if (key !== `${invocationKey}/${laneId}`) invalid('WORK_CTRL_LANE_IDENTITY_MISMATCH', itemLocation, 'Lane map key must equal invocationKey/laneId.');
@@ -583,12 +593,22 @@ function normalizeWorkCtrlCancellations(value: unknown, location: string): void
583
593
  }
584
594
  }
585
595
 
586
- function normalizeWorkCtrlComposites(value: unknown, location: string): void {
596
+ function normalizeWorkCtrlComposites(value: unknown, location: string, lanes: Map<string, { invocationKey: string; laneId: string; status: string }>): void {
587
597
  for (const [key, item, itemLocation] of closedRecordEntries(value, location)) {
588
- const fact = exactRecord(item, ['invocationKey', 'primitive', 'bodyId', 'status', 'updatedAtMs'], ['input', 'cursor', 'childTreeId'], itemLocation);
598
+ const fact = exactRecord(item, ['invocationKey', 'primitive', 'bodyId', 'status', 'updatedAtMs'], ['input', 'cursor', 'childTreeId', 'expansion'], itemLocation);
589
599
  assertFactKey(fact, key, itemLocation);
590
600
  const primitive = dataField(fact, 'primitive', `${itemLocation}.primitive`);
591
601
  if (!['Retry', 'Timeout', 'ForEach', 'Parallel', 'Race', 'CallFlow'].includes(String(primitive))) invalid('WORK_CTRL_COMPOSITE_PRIMITIVE_INVALID', `${itemLocation}.primitive`, 'Composite primitive is invalid.');
602
+ if (Object.hasOwn(fact, 'expansion')) {
603
+ const location = `${itemLocation}.expansion`;
604
+ const expansion = exactRecord(dataField(fact, 'expansion', location), ['itemCount', 'concurrency'], ['maxItems'], location);
605
+ const itemCount = nonNegativeSafeInteger(dataField(expansion, 'itemCount', `${location}.itemCount`), `${location}.itemCount`);
606
+ const concurrency = nonNegativeSafeInteger(dataField(expansion, 'concurrency', `${location}.concurrency`), `${location}.concurrency`);
607
+ const maxItems = Object.hasOwn(expansion, 'maxItems') ? nonNegativeSafeInteger(dataField(expansion, 'maxItems', `${location}.maxItems`), `${location}.maxItems`) : undefined;
608
+ if (primitive !== 'ForEach' || concurrency < 1 || concurrency > Math.min(32, maxItems ?? 1000) || (maxItems !== undefined && (maxItems < 1 || maxItems > 1000 || itemCount > maxItems))) invalid('WORK_CTRL_EXPANSION_INVALID', location, 'Frozen ForEach expansion exceeds its budget.');
609
+ const expanded = [...lanes.values()].filter(lane => lane.invocationKey === key);
610
+ if (expanded.length !== itemCount || Array.from({ length: itemCount }, (_, index) => `${key}/${index}`).some(id => !lanes.has(id))) invalid('WORK_CTRL_EXPANSION_INCOMPLETE', location, 'Frozen ForEach lanes must cover the exact input order.');
611
+ }
592
612
  canonicalIdentity(dataField(fact, 'bodyId', `${itemLocation}.bodyId`), `${itemLocation}.bodyId`);
593
613
  if (!['Open', 'Waiting', 'Completed', 'Failed', 'Cancelled'].includes(String(dataField(fact, 'status', `${itemLocation}.status`)))) invalid('WORK_CTRL_COMPOSITE_STATUS_INVALID', `${itemLocation}.status`, 'Composite status is invalid.');
594
614
  nonNegativeSafeInteger(dataField(fact, 'updatedAtMs', `${itemLocation}.updatedAtMs`), `${itemLocation}.updatedAtMs`);
@@ -1412,7 +1432,7 @@ function normalizeAIDataNodeResult(
1412
1432
  const result = exactRecord(
1413
1433
  value,
1414
1434
  ['nodeId', 'generation', 'status'],
1415
- ['output', 'semanticFingerprint', 'reusedFrom'],
1435
+ ['output', 'semanticFingerprint', 'reusedFrom', 'waitContext'],
1416
1436
  location,
1417
1437
  );
1418
1438
  if (canonicalIdentity(dataField(result, 'nodeId', `${location}.nodeId`), `${location}.nodeId`) !== nodeId) {
@@ -1422,6 +1442,10 @@ function normalizeAIDataNodeResult(
1422
1442
  invalid('AI_DATA_RESULT_GENERATION_INVALID', `${location}.generation`, 'Result generation must equal its run node generation.');
1423
1443
  }
1424
1444
  const resultStatus = String(dataField(result, 'status', `${location}.status`));
1445
+ if(Object.hasOwn(result,'waitContext')){
1446
+ const context=normalizeFlowClosedObject(dataField(result,'waitContext',`${location}.waitContext`),`${location}.waitContext`);
1447
+ if(new TextEncoder().encode(JSON.stringify(context)).length>16384)invalid('AI_DATA_WAIT_CONTEXT_BUDGET',`${location}.waitContext`,'Native wait context exceeds 16KiB.');
1448
+ }
1425
1449
  if (!['Pending', 'Running', 'Waiting', 'Succeeded', 'Failed', 'Cancelled', 'Invalidated', 'Reused'].includes(resultStatus)) {
1426
1450
  invalid('AI_DATA_RESULT_STATUS_INVALID', `${location}.status`, 'Result status is not part of the public contract.');
1427
1451
  }
package/src/run-freeze.ts CHANGED
@@ -7,6 +7,8 @@ import type {
7
7
  AIWorkflowRunResourceFreezeReceipt,
8
8
  MaterialBindingProjection,
9
9
  MaterialPortProjection,
10
+ CompiledAIAgentCodeExecution,
11
+ AIAgentCodeExecutionIdentity,
10
12
  } from 'ai-workflow-contract';
11
13
  import {
12
14
  normalizeHolonTaskTarget,
@@ -19,18 +21,24 @@ import {
19
21
  composeLayeredResourceRegistry,
20
22
  loadResourceTree,
21
23
  sha256Digest,
24
+ digestCanonical,
22
25
  type EffectiveResourceRegistry,
23
26
  type ResourceContentIdentity,
24
27
  type ResourceDependencyEdge,
28
+ type Sha256Digest,
25
29
  } from 'halfcode-compiler.xnl/resource-core';
26
30
  import { projectAIWorkflowAgentResources } from './agent-resources.js';
27
31
  import { sourceRegistryForAIWorkflowAgentResourceProjection } from './agent-resource-brand.js';
32
+ import { authorityForAIAgentCodeExecution } from './agent-code-brand.js';
33
+ import { assertAIAgentExecutionResources, assertAIAgentFreezeRegistry, type AIAgentExecutionResourceObservation } from './agent-execution-resources.js';
28
34
  import {
29
35
  createResolvedAIWorkflowContentIdentityIndex,
30
36
  resolveAIWorkflowResourceTree,
31
37
  } from './resource-resolution.js';
32
38
 
33
39
  export interface FreezeAIWorkflowRunResourcesInput {
40
+ readonly executionResources?: AIAgentExecutionResourceObservation;
41
+ readonly codeExecutions?: readonly CompiledAIAgentCodeExecution[];
34
42
  readonly registry: EffectiveResourceRegistry;
35
43
  readonly projection: AIWorkflowAgentResourceProjection;
36
44
  readonly task: AIWorkflowAgentTaskRef;
@@ -63,6 +71,7 @@ export class AIWorkflowRunResourceFreezeError extends Error {
63
71
 
64
72
  const authenticFreezeReceipts = new WeakSet<object>();
65
73
  const authenticTaskBindings = new WeakSet<object>();
74
+ const receiptByTaskBinding = new WeakMap<object, AIWorkflowRunResourceFreezeReceipt>();
66
75
  const authenticHolonTargetFreezeReceipts = new WeakSet<object>();
67
76
  const authenticFrozenHolonTargets = new WeakSet<object>();
68
77
 
@@ -185,6 +194,7 @@ export function assertFrozenHolonTaskTarget(value: FrozenHolonTaskTarget): Froze
185
194
  export function freezeAIWorkflowRunResources(
186
195
  input: FreezeAIWorkflowRunResourcesInput,
187
196
  ): AIWorkflowRunResourceFreezeReceipt {
197
+ assertAIAgentFreezeRegistry(input.registry);
188
198
  const diagnostics: AIWorkflowResourceProjectionDiagnostic[] = [];
189
199
  if (sourceRegistryForAIWorkflowAgentResourceProjection(input.projection) !== input.registry) {
190
200
  diagnostics.push(diagnostic(
@@ -194,6 +204,19 @@ export function freezeAIWorkflowRunResources(
194
204
  ));
195
205
  throwFreezeError(diagnostics);
196
206
  }
207
+ const executionResources = input.executionResources === undefined ? undefined
208
+ : assertAIAgentExecutionResources(input.executionResources, input.registry);
209
+ if (executionResources) {
210
+ if (input.contentIdentities.size !== executionResources.contentIdentities.size
211
+ || [...executionResources.contentIdentities].some(([id, identity]) =>
212
+ digestCanonical(input.contentIdentities.get(id) ?? null) !== digestCanonical(identity))) {
213
+ throw new AIWorkflowRunResourceFreezeError(Object.freeze([
214
+ diagnostic('AI_RUN_EXECUTION_CONTENT_IDENTITY_MISMATCH', 'run-freeze:identities', 'Execution observation requires its exact authored content identity facts.'),
215
+ ]));
216
+ }
217
+ input = { ...input, registry: executionResources.registry, projection: executionResources.projection,
218
+ contentIdentities: executionResources.contentIdentities };
219
+ }
197
220
  const task = normalizeTask(input.task, diagnostics);
198
221
  if (!task) throwFreezeError(diagnostics);
199
222
  const workflowId = resourceIdFromRef(task.workflowRef)!;
@@ -230,19 +253,34 @@ export function freezeAIWorkflowRunResources(
230
253
  agentDefinitionRef: task.agentDefinitionRef,
231
254
  });
232
255
  const bindingResourceIds = Object.freeze(orderedBindings.map((binding) => binding.resource.resourceId));
233
- const semanticFingerprint = semanticResourceFingerprint(
256
+ const resourceFingerprint = semanticResourceFingerprint(
234
257
  frozenTask,
235
258
  agent,
236
259
  orderedBindings,
237
260
  dependencySnapshot.closure,
238
261
  dependencySnapshot.edges,
239
262
  );
263
+ const codeExecutions = selectedCodeExecutions(input, dependencySnapshot.closure.map(item => item.resourceId));
264
+ const resourceResolutions = executionResources && Object.freeze(dependencySnapshot.closure
265
+ .map(item => {
266
+ const resolution = executionResources.resolutionIdentities.get(item.resourceId);
267
+ if (!resolution) throw new Error(`AI_RUN_RESOURCE_RESOLUTION_MISSING: ${item.resourceId}`);
268
+ return resolution;
269
+ }).sort((a, b) => a.resourceId < b.resourceId ? -1 : a.resourceId > b.resourceId ? 1 : 0));
270
+ const codeFingerprint = codeExecutions.length
271
+ ? sha256Digest(JSON.stringify({ schemaVersion: 'ai-workflow.code-bound-semantic/v1', resourceFingerprint, codeExecutions }))
272
+ : resourceFingerprint;
273
+ const semanticFingerprint = resourceResolutions
274
+ ? sha256Digest(JSON.stringify({ schemaVersion: 'ai-workflow.reader-bound-semantic/v1', resourceFingerprint: codeFingerprint, resourceResolutions }))
275
+ : codeFingerprint;
240
276
  const receipt = Object.freeze({
241
277
  schemaVersion: 'ai-workflow.run-resource-freeze/v1',
242
278
  task: frozenTask,
243
279
  bindingResourceIds,
244
280
  dependencySnapshot,
245
281
  semanticFingerprint,
282
+ ...(codeExecutions.length ? { codeExecutions } : {}),
283
+ ...(resourceResolutions ? { resourceResolutions } : {}),
246
284
  });
247
285
  authenticFreezeReceipts.add(receipt);
248
286
  return receipt;
@@ -263,11 +301,52 @@ export function projectFrozenAIAgentTaskBinding(
263
301
  semanticFingerprint: receipt.semanticFingerprint,
264
302
  snapshotRevision: receipt.dependencySnapshot.snapshotRevision,
265
303
  closureResourceIds: Object.freeze(receipt.dependencySnapshot.closure.map((item) => item.resourceId)),
304
+ ...(receipt.codeExecutions ? { codeExecutions: receipt.codeExecutions } : {}),
305
+ ...(receipt.resourceResolutions ? { resourceResolutions: receipt.resourceResolutions } : {}),
266
306
  });
267
307
  authenticTaskBindings.add(binding);
308
+ receiptByTaskBinding.set(binding, receipt);
268
309
  return binding;
269
310
  }
270
311
 
312
+ /** Retrieves the old Agent identity from the authentic freeze, not caller feedback. */
313
+ export function frozenAIAgentTaskAgentContentDigest(binding: FrozenAIAgentTaskBinding): Sha256Digest {
314
+ assertFrozenAIAgentTaskBinding(binding);
315
+ const receipt = receiptByTaskBinding.get(binding)!;
316
+ const agentId = resourceIdFromRef(receipt.task.agentDefinitionRef)!;
317
+ const identity = receipt.dependencySnapshot.closure.find(({ resourceId }) => resourceId === agentId);
318
+ if (!identity) throw new AIWorkflowRunResourceFreezeError(Object.freeze([
319
+ diagnostic('AI_RUN_AGENT_IDENTITY_MISSING', binding.task.agentDefinitionRef, 'Frozen Agent identity is absent.'),
320
+ ]));
321
+ if (!/^sha256:[a-f0-9]{64}$/.test(identity.contentDigest)) throw new Error('Invalid frozen Agent content digest');
322
+ return identity.contentDigest as Sha256Digest;
323
+ }
324
+
325
+ function selectedCodeExecutions(input: FreezeAIWorkflowRunResourcesInput, closureIds: readonly string[]): readonly AIAgentCodeExecutionIdentity[] {
326
+ const selected = new Set(closureIds);
327
+ const identities = new Map<string, AIAgentCodeExecutionIdentity>();
328
+ for (const execution of input.codeExecutions ?? []) {
329
+ const authority = authorityForAIAgentCodeExecution(execution);
330
+ if (!authority || !selected.has(authority.identity.resourceId)
331
+ || input.registry.byId.get(authority.identity.resourceId)?.resource !== authority.projection.resource
332
+ || !(input.projection.codeResources ?? []).includes(authority.projection)
333
+ || identities.has(authority.identity.resourceId)) {
334
+ throw new AIWorkflowRunResourceFreezeError(Object.freeze([
335
+ diagnostic('AI_RUN_CODE_EXECUTION_UNTRUSTED', 'run-freeze:code', 'Code execution must be authentic, selected and belong to the exact registry projection.'),
336
+ ]));
337
+ }
338
+ identities.set(authority.identity.resourceId, authority.identity);
339
+ }
340
+ for (const code of input.projection.codeResources ?? []) {
341
+ if (code.mode === 'native' && selected.has(code.resource.resourceId) && !identities.has(code.resource.resourceId)) {
342
+ throw new AIWorkflowRunResourceFreezeError(Object.freeze([
343
+ diagnostic('AI_RUN_CODE_EXECUTION_MISSING', `resource://${code.resource.resourceId}`, 'Native code must be compiled or restored before task freeze.'),
344
+ ]));
345
+ }
346
+ }
347
+ return Object.freeze([...identities.values()].sort((a, b) => a.resourceId < b.resourceId ? -1 : a.resourceId > b.resourceId ? 1 : 0));
348
+ }
349
+
271
350
  /** Runtime assertion; a structurally similar literal is not an admitted authority. */
272
351
  export function assertFrozenAIAgentTaskBinding(value: FrozenAIAgentTaskBinding): FrozenAIAgentTaskBinding {
273
352
  if (!authenticTaskBindings.has(value as object)) {