ai-workflow-logic 0.1.11 → 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.
@@ -0,0 +1,145 @@
1
+ import Ajv, { type ValidateFunction } from 'ajv';
2
+ import {
3
+ DEPA_AI_RESOURCE_KIND_OWNERS,
4
+ DEPA_AI_RESOURCE_KIND_READER_REGISTRATIONS,
5
+ DEPA_AI_RESOURCE_KIND_SPEC_REVISIONS,
6
+ DEPA_AI_RESOURCE_KIND_SPEC_RESOLUTIONS,
7
+ } from 'ai-workflow-contract';
8
+ import type {
9
+ AuthoredResourceTree,
10
+ JsonSchemaValidator,
11
+ KindSpecRevision,
12
+ PortableSpec,
13
+ ResolvedResourceTree,
14
+ ResourceContentIdentity,
15
+ } from 'halfcode-compiler.xnl/resource-core';
16
+ import { createResourceContentIdentity, digestCanonical } from 'halfcode-compiler.xnl/resource-core';
17
+ import {
18
+ CORE_KIND_READER_REGISTRATIONS,
19
+ CORE_KIND_SPEC_REVISIONS,
20
+ CORE_KIND_SUBJECT_OWNERS,
21
+ KindContractRegistry,
22
+ KindReaderRegistry,
23
+ SpecResolutionRegistry,
24
+ createReaderProfile,
25
+ resolveResourceTree,
26
+ type ReaderProfile,
27
+ } from 'halfcode-compiler.xnl/kind-definition';
28
+
29
+ export const DEPA_AI_RESOURCE_READER_PROFILE_ID = 'depa.flows.ai-resources.v1' as const;
30
+
31
+ export interface AIWorkflowResourceResolutionContext {
32
+ readonly contracts: KindContractRegistry;
33
+ readonly readers: KindReaderRegistry;
34
+ readonly readerProfile: ReaderProfile;
35
+ readonly resolutions: SpecResolutionRegistry;
36
+ readonly validator: JsonSchemaValidator;
37
+ }
38
+
39
+ const ajv = new Ajv({ allErrors: true, strict: false, validateFormats: false });
40
+ const compiledSchemas = new Map<string, ValidateFunction>();
41
+
42
+ export const AI_WORKFLOW_RESOURCE_SCHEMA_VALIDATOR: JsonSchemaValidator = Object.freeze({
43
+ validate(revision: KindSpecRevision, value: PortableSpec): readonly string[] {
44
+ let validate = compiledSchemas.get(revision.contractFingerprint);
45
+ if (!validate) {
46
+ validate = ajv.compile(revision.specSchema);
47
+ compiledSchemas.set(revision.contractFingerprint, validate);
48
+ }
49
+ return validate(value)
50
+ ? Object.freeze([])
51
+ : Object.freeze((validate.errors ?? []).map((error) =>
52
+ `${error.instancePath || '/'} ${error.message ?? 'invalid'}`));
53
+ },
54
+ });
55
+
56
+ export function createAIWorkflowResourceResolutionContext(
57
+ validator: JsonSchemaValidator = AI_WORKFLOW_RESOURCE_SCHEMA_VALIDATOR,
58
+ tree?: AuthoredResourceTree,
59
+ ): AIWorkflowResourceResolutionContext {
60
+ const contracts = new KindContractRegistry();
61
+ for (const owner of [...CORE_KIND_SUBJECT_OWNERS, ...DEPA_AI_RESOURCE_KIND_OWNERS]) {
62
+ contracts.registerOwner(owner);
63
+ }
64
+ // A package's declared revision set is an exact Halfcode admission boundary.
65
+ // Old packages keep their original v1 profile; new packages declare v1+v2.
66
+ const revisions = [...CORE_KIND_SPEC_REVISIONS, ...DEPA_AI_RESOURCE_KIND_SPEC_REVISIONS].filter(revision => {
67
+ const definition = tree && [...tree.registry.kindDefinitions.values()].find(item => item.subjectFqn === revision.subjectFqn);
68
+ return definition
69
+ ? definition.specRevisions.some(item => item.specVersion === revision.specVersion)
70
+ : revision.specVersion === 1;
71
+ });
72
+ for (const revision of revisions) {
73
+ contracts.registerRevision(revision);
74
+ }
75
+ const hasRevision = (subjectFqn: string, specVersion: number): boolean => revisions.some(revision =>
76
+ revision.subjectFqn === subjectFqn && revision.specVersion === specVersion);
77
+
78
+ const readerRegistrations = [
79
+ ...CORE_KIND_READER_REGISTRATIONS,
80
+ ...DEPA_AI_RESOURCE_KIND_READER_REGISTRATIONS,
81
+ ].filter(reader => hasRevision(reader.subjectFqn, reader.readerSpecVersion));
82
+ const readers = new KindReaderRegistry();
83
+ for (const reader of readerRegistrations) readers.register(reader);
84
+ const selectedReaders = readerRegistrations.filter(reader => !readerRegistrations.some(other =>
85
+ other.subjectFqn === reader.subjectFqn && other.readerSpecVersion > reader.readerSpecVersion));
86
+ const targets = selectedReaders.map((reader) => ({
87
+ readerId: reader.readerId,
88
+ subjectFqn: reader.subjectFqn,
89
+ readerSpecVersion: reader.readerSpecVersion,
90
+ contractFingerprint: reader.contractFingerprint,
91
+ readerImplementationFingerprint: reader.readerImplementationFingerprint,
92
+ }));
93
+ const readerProfile = createReaderProfile(
94
+ targets.some(target => target.readerSpecVersion !== 1)
95
+ ? `${DEPA_AI_RESOURCE_READER_PROFILE_ID}/${digestCanonical(targets)}`
96
+ : DEPA_AI_RESOURCE_READER_PROFILE_ID,
97
+ targets,
98
+ );
99
+ const resolutions = new SpecResolutionRegistry(contracts);
100
+ for (const resolution of DEPA_AI_RESOURCE_KIND_SPEC_RESOLUTIONS) {
101
+ if (hasRevision(resolution.subjectFqn, resolution.writerSpecVersion)
102
+ && hasRevision(resolution.subjectFqn, resolution.readerSpecVersion)) {
103
+ resolutions.register(resolution);
104
+ }
105
+ }
106
+ return Object.freeze({
107
+ contracts,
108
+ readers,
109
+ readerProfile,
110
+ resolutions,
111
+ validator,
112
+ });
113
+ }
114
+
115
+ export function resolveAIWorkflowResourceTree(
116
+ tree: AuthoredResourceTree,
117
+ context?: AIWorkflowResourceResolutionContext,
118
+ ): ResolvedResourceTree {
119
+ return resolveResourceTree({ tree, ...(context ?? createAIWorkflowResourceResolutionContext(undefined, tree)) });
120
+ }
121
+
122
+ /**
123
+ * Projects Halfcode's exact resolved content identity into the public dependency
124
+ * snapshot shape. The digest is still constructed by Halfcode; depa-flows only
125
+ * chooses the resolved receipt identity as the authority input.
126
+ */
127
+ export function createResolvedAIWorkflowContentIdentityIndex(
128
+ tree: ResolvedResourceTree,
129
+ ): ReadonlyMap<string, ResourceContentIdentity> {
130
+ const entries: Array<readonly [string, ResourceContentIdentity]> = [];
131
+ for (const records of tree.registry.byKind.values()) {
132
+ for (const record of records) {
133
+ entries.push([record.resourceId, createResourceContentIdentity({
134
+ resourceId: record.resourceId,
135
+ authorityDigest: record.effectiveContentDigest,
136
+ })]);
137
+ }
138
+ }
139
+ entries.sort(([left], [right]) => compareCodeUnits(left, right));
140
+ return new Map(entries);
141
+ }
142
+
143
+ function compareCodeUnits(left: string, right: string): number {
144
+ return left < right ? -1 : left > right ? 1 : 0;
145
+ }
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,
@@ -18,16 +20,25 @@ import {
18
20
  buildResourceDependencySnapshot,
19
21
  composeLayeredResourceRegistry,
20
22
  loadResourceTree,
21
- resolveEffectiveResourceContentIdentities,
22
23
  sha256Digest,
24
+ digestCanonical,
23
25
  type EffectiveResourceRegistry,
24
26
  type ResourceContentIdentity,
25
27
  type ResourceDependencyEdge,
28
+ type Sha256Digest,
26
29
  } from 'halfcode-compiler.xnl/resource-core';
27
30
  import { projectAIWorkflowAgentResources } from './agent-resources.js';
28
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';
34
+ import {
35
+ createResolvedAIWorkflowContentIdentityIndex,
36
+ resolveAIWorkflowResourceTree,
37
+ } from './resource-resolution.js';
29
38
 
30
39
  export interface FreezeAIWorkflowRunResourcesInput {
40
+ readonly executionResources?: AIAgentExecutionResourceObservation;
41
+ readonly codeExecutions?: readonly CompiledAIAgentCodeExecution[];
31
42
  readonly registry: EffectiveResourceRegistry;
32
43
  readonly projection: AIWorkflowAgentResourceProjection;
33
44
  readonly task: AIWorkflowAgentTaskRef;
@@ -39,14 +50,15 @@ export async function freezeAIWorkflowRunResourcesFromDirectory(
39
50
  rootDir: string,
40
51
  task: AIWorkflowAgentTaskRef,
41
52
  ): Promise<AIWorkflowRunResourceFreezeReceipt> {
42
- const tree = await loadResourceTree({ rootDir });
53
+ const authoredTree = await loadResourceTree({ rootDir });
54
+ const tree = resolveAIWorkflowResourceTree(authoredTree);
43
55
  const layers = [{ id: 'workspace', tree }] as const;
44
56
  const registry = composeLayeredResourceRegistry({ layers });
45
57
  return freezeAIWorkflowRunResources({
46
58
  registry,
47
59
  projection: projectAIWorkflowAgentResources(registry),
48
60
  task,
49
- contentIdentities: resolveEffectiveResourceContentIdentities({ registry, layers }),
61
+ contentIdentities: createResolvedAIWorkflowContentIdentityIndex(tree),
50
62
  });
51
63
  }
52
64
 
@@ -59,6 +71,7 @@ export class AIWorkflowRunResourceFreezeError extends Error {
59
71
 
60
72
  const authenticFreezeReceipts = new WeakSet<object>();
61
73
  const authenticTaskBindings = new WeakSet<object>();
74
+ const receiptByTaskBinding = new WeakMap<object, AIWorkflowRunResourceFreezeReceipt>();
62
75
  const authenticHolonTargetFreezeReceipts = new WeakSet<object>();
63
76
  const authenticFrozenHolonTargets = new WeakSet<object>();
64
77
 
@@ -72,13 +85,14 @@ export async function freezeAIWorkflowHolonTaskTargetFromDirectory(
72
85
  rootDir: string,
73
86
  target: HolonTaskTarget,
74
87
  ): Promise<AIWorkflowHolonTaskTargetFreezeReceipt> {
75
- const tree = await loadResourceTree({ rootDir });
88
+ const authoredTree = await loadResourceTree({ rootDir });
89
+ const tree = resolveAIWorkflowResourceTree(authoredTree);
76
90
  const layers = [{ id: 'workspace', tree }] as const;
77
91
  const registry = composeLayeredResourceRegistry({ layers });
78
92
  return freezeAIWorkflowHolonTaskTarget({
79
93
  registry,
80
94
  target,
81
- contentIdentities: resolveEffectiveResourceContentIdentities({ registry, layers }),
95
+ contentIdentities: createResolvedAIWorkflowContentIdentityIndex(tree),
82
96
  });
83
97
  }
84
98
 
@@ -180,6 +194,7 @@ export function assertFrozenHolonTaskTarget(value: FrozenHolonTaskTarget): Froze
180
194
  export function freezeAIWorkflowRunResources(
181
195
  input: FreezeAIWorkflowRunResourcesInput,
182
196
  ): AIWorkflowRunResourceFreezeReceipt {
197
+ assertAIAgentFreezeRegistry(input.registry);
183
198
  const diagnostics: AIWorkflowResourceProjectionDiagnostic[] = [];
184
199
  if (sourceRegistryForAIWorkflowAgentResourceProjection(input.projection) !== input.registry) {
185
200
  diagnostics.push(diagnostic(
@@ -189,6 +204,19 @@ export function freezeAIWorkflowRunResources(
189
204
  ));
190
205
  throwFreezeError(diagnostics);
191
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
+ }
192
220
  const task = normalizeTask(input.task, diagnostics);
193
221
  if (!task) throwFreezeError(diagnostics);
194
222
  const workflowId = resourceIdFromRef(task.workflowRef)!;
@@ -225,19 +253,34 @@ export function freezeAIWorkflowRunResources(
225
253
  agentDefinitionRef: task.agentDefinitionRef,
226
254
  });
227
255
  const bindingResourceIds = Object.freeze(orderedBindings.map((binding) => binding.resource.resourceId));
228
- const semanticFingerprint = semanticResourceFingerprint(
256
+ const resourceFingerprint = semanticResourceFingerprint(
229
257
  frozenTask,
230
258
  agent,
231
259
  orderedBindings,
232
260
  dependencySnapshot.closure,
233
261
  dependencySnapshot.edges,
234
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;
235
276
  const receipt = Object.freeze({
236
277
  schemaVersion: 'ai-workflow.run-resource-freeze/v1',
237
278
  task: frozenTask,
238
279
  bindingResourceIds,
239
280
  dependencySnapshot,
240
281
  semanticFingerprint,
282
+ ...(codeExecutions.length ? { codeExecutions } : {}),
283
+ ...(resourceResolutions ? { resourceResolutions } : {}),
241
284
  });
242
285
  authenticFreezeReceipts.add(receipt);
243
286
  return receipt;
@@ -258,11 +301,52 @@ export function projectFrozenAIAgentTaskBinding(
258
301
  semanticFingerprint: receipt.semanticFingerprint,
259
302
  snapshotRevision: receipt.dependencySnapshot.snapshotRevision,
260
303
  closureResourceIds: Object.freeze(receipt.dependencySnapshot.closure.map((item) => item.resourceId)),
304
+ ...(receipt.codeExecutions ? { codeExecutions: receipt.codeExecutions } : {}),
305
+ ...(receipt.resourceResolutions ? { resourceResolutions: receipt.resourceResolutions } : {}),
261
306
  });
262
307
  authenticTaskBindings.add(binding);
308
+ receiptByTaskBinding.set(binding, receipt);
263
309
  return binding;
264
310
  }
265
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
+
266
350
  /** Runtime assertion; a structurally similar literal is not an admitted authority. */
267
351
  export function assertFrozenAIAgentTaskBinding(value: FrozenAIAgentTaskBinding): FrozenAIAgentTaskBinding {
268
352
  if (!authenticTaskBindings.has(value as object)) {