ai-workflow-logic 0.1.1 → 0.1.3

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-workflow-logic",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "description": "AI workflow domain projections and pure runtime helpers with an explicit Halfcode filesystem adapter",
6
6
  "type": "module",
@@ -12,14 +12,13 @@
12
12
  "./run-freeze": "./src/run-freeze.ts"
13
13
  },
14
14
  "dependencies": {
15
- "ai-workflow-contract": "workspace:*",
15
+ "ai-workflow-contract": "0.1.3",
16
16
  "halfcode-compiler.xnl": "0.2.2"
17
17
  },
18
18
  "files": [
19
19
  "src"
20
20
  ],
21
21
  "publishConfig": {
22
- "access": "public",
23
- "registry": "https://registry.npmjs.com"
22
+ "access": "public"
24
23
  }
25
24
  }
@@ -2,10 +2,13 @@ import type {
2
2
  AIAgentDefinitionProjection,
3
3
  AIAgentMessageProjection,
4
4
  AIAgentMessageRole,
5
+ AIWorkflowEffectPolicyProjection,
5
6
  AIWorkflowAgentResourceProjection,
6
7
  AIWorkflowAgentResourceRelation,
7
8
  AIWorkflowAgentTaskRef,
8
9
  AIWorkflowDefinitionResourceKind,
10
+ AIWorkflowMaterialValueProjection,
11
+ AIWorkflowMessageSchemaProjection,
9
12
  AIWorkflowResourceProjectionDiagnostic,
10
13
  AIWorkflowResourceRegistryView,
11
14
  AIWorkflowTypedResourceRef,
@@ -114,7 +117,7 @@ function projectMaterialPort(
114
117
  diagnostics.push(diagnostic('AI_MATERIAL_PORT_CARDINALITY_INVALID', resource.documentUri, 'MaterialPort cardinality must be one or many.'));
115
118
  valid = false;
116
119
  }
117
- const schema = projectOptionalTypedSubdomain(
120
+ const schema = projectOptionalSchemaSubdomain(
118
121
  resource,
119
122
  'SchemaRef',
120
123
  resources,
@@ -147,12 +150,12 @@ function projectAgentDefinition(
147
150
  const messages = messagesNode
148
151
  ? projectMessages(resource, messagesNode, resources, edges, diagnostics)
149
152
  : [];
150
- const inputSchema = projectOptionalTypedSubdomain(resource, 'InputSchemaRef', resources, 'agent-input-schema', edges, diagnostics);
151
- const outputSchema = projectOptionalTypedSubdomain(resource, 'OutputSchemaRef', resources, 'agent-output-schema', edges, diagnostics);
153
+ const inputSchema = projectOptionalSchemaSubdomain(resource, 'InputSchemaRef', resources, 'agent-input-schema', edges, diagnostics);
154
+ const outputSchema = projectOptionalSchemaSubdomain(resource, 'OutputSchemaRef', resources, 'agent-output-schema', edges, diagnostics);
152
155
  const tools = toolRefsNode
153
156
  ? projectTypedRefList(resource, toolRefsNode, 'ToolRef', resources, 'agent-tool', edges, diagnostics)
154
157
  : [];
155
- const effectPolicy = projectOptionalTypedSubdomain(resource, 'EffectPolicyRef', resources, 'agent-effect-policy', edges, diagnostics);
158
+ const effectPolicy = projectOptionalEffectPolicySubdomain(resource, 'EffectPolicyRef', resources, 'agent-effect-policy', edges, diagnostics);
156
159
  const materialPorts = materialPortRefsNode
157
160
  ? projectTypedRefList(resource, materialPortRefsNode, 'MaterialPortRef', resources, 'agent-material-port', edges, diagnostics, 'MaterialPort')
158
161
  : [];
@@ -209,7 +212,7 @@ function projectMessages(
209
212
  if (prompt) edges.push(edge(resource.resourceId, prompt.resource.resourceId, 'agent-message-prompt'));
210
213
  const schemaNode = value.subdomains.SchemaRef;
211
214
  const schema = schemaNode?.tag === 'SchemaRef'
212
- ? projectTypedRefNode(schemaNode, resources, `${location}#SchemaRef`, diagnostics)
215
+ ? projectMessageSchemaRefNode(schemaNode, resources, `${location}#SchemaRef`, diagnostics)
213
216
  : undefined;
214
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}'.`));
215
218
  if (schema) edges.push(edge(resource.resourceId, schema.resource.resourceId, 'agent-message-schema'));
@@ -274,6 +277,69 @@ function projectOptionalTypedSubdomain(
274
277
  return typedRef;
275
278
  }
276
279
 
280
+ function projectOptionalSchemaSubdomain(
281
+ owner: ResourceRecord,
282
+ name: string,
283
+ resources: ReadonlyMap<string, ResourceRecord>,
284
+ relation: AIWorkflowAgentResourceRelation,
285
+ edges: ResourceDependencyEdge[],
286
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
287
+ ): AIWorkflowMessageSchemaProjection | undefined {
288
+ const node = owner.node.subdomains[name];
289
+ if (!node) return undefined;
290
+ if (node.tag !== name) {
291
+ diagnostics.push(diagnostic('AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH', `${owner.documentUri}#${name}`, `Subdomain ${name} must contain a ${name} node, got '${node.tag}'.`));
292
+ return undefined;
293
+ }
294
+ const schema = projectMessageSchemaRefNode(node, resources, `${owner.documentUri}#${name}`, diagnostics);
295
+ if (schema) edges.push(edge(owner.resourceId, schema.resource.resourceId, relation));
296
+ return schema;
297
+ }
298
+
299
+ function projectOptionalEffectPolicySubdomain(
300
+ owner: ResourceRecord,
301
+ name: string,
302
+ resources: ReadonlyMap<string, ResourceRecord>,
303
+ relation: AIWorkflowAgentResourceRelation,
304
+ edges: ResourceDependencyEdge[],
305
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
306
+ ): AIWorkflowEffectPolicyProjection | undefined {
307
+ const node = owner.node.subdomains[name];
308
+ if (!node) return undefined;
309
+ if (node.tag !== name) {
310
+ diagnostics.push(diagnostic('AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH', `${owner.documentUri}#${name}`, `Subdomain ${name} must contain a ${name} node, got '${node.tag}'.`));
311
+ return undefined;
312
+ }
313
+ const typedRef = projectTypedRefNode(node, resources, `${owner.documentUri}#${name}`, diagnostics, 'EffectPolicy');
314
+ if (!typedRef) return undefined;
315
+ const supportedProperties = new Set(['lifecycle', 'description', 'toolMode']);
316
+ const unsupportedProperties = Reflect.ownKeys(typedRef.resource.node.properties)
317
+ .filter((key) => typeof key !== 'string' || !supportedProperties.has(key))
318
+ .map((key) => typeof key === 'string' ? key : key.toString())
319
+ .sort(compareCodeUnits);
320
+ if (unsupportedProperties.length > 0) {
321
+ for (const property of unsupportedProperties) {
322
+ diagnostics.push(diagnostic(
323
+ 'AI_EFFECT_POLICY_PROPERTY_UNSUPPORTED',
324
+ `${typedRef.resource.documentUri}#${property}`,
325
+ `EffectPolicy property '${property}' is not part of the closed executable policy contract.`,
326
+ ));
327
+ }
328
+ return undefined;
329
+ }
330
+ const toolMode = ownResourceValue(typedRef.resource.node.properties, 'toolMode');
331
+ if (toolMode !== 'declared-only' && toolMode !== 'none') {
332
+ diagnostics.push(diagnostic(
333
+ 'AI_EFFECT_POLICY_TOOL_MODE_INVALID',
334
+ typedRef.resource.documentUri,
335
+ "EffectPolicy toolMode must be 'declared-only' or 'none'.",
336
+ ));
337
+ return undefined;
338
+ }
339
+ edges.push(edge(owner.resourceId, typedRef.resource.resourceId, relation));
340
+ return Object.freeze({ ...typedRef, kind: 'EffectPolicy', toolMode });
341
+ }
342
+
277
343
  function projectMaterialBinding(
278
344
  resource: ResourceRecord,
279
345
  resources: ReadonlyMap<string, ResourceRecord>,
@@ -287,7 +353,10 @@ function projectMaterialBinding(
287
353
  const materialNode = requiredSubdomain(resource, 'MaterialRef', diagnostics);
288
354
  const task = taskNode ? projectTaskRef(taskNode, resources, `${resource.documentUri}#AgentTaskRef`, diagnostics) : undefined;
289
355
  const portRef = portNode ? projectTypedRefNode(portNode, resources, `${resource.documentUri}#PortRef`, diagnostics, 'MaterialPort') : undefined;
290
- const material = materialNode ? projectTypedRefNode(materialNode, resources, `${resource.documentUri}#MaterialRef`, diagnostics) : undefined;
356
+ const materialRef = materialNode ? projectTypedRefNode(materialNode, resources, `${resource.documentUri}#MaterialRef`, diagnostics) : undefined;
357
+ const material = materialRef
358
+ ? projectMaterialValue(materialRef, diagnostics)
359
+ : undefined;
291
360
  if (portRef) edges.push(edge(resource.resourceId, portRef.resource.resourceId, 'material-binding-port'));
292
361
  if (material) edges.push(edge(resource.resourceId, material.resource.resourceId, 'material-binding-material'));
293
362
  const port = portRef ? portsById.get(portRef.resource.resourceId) : undefined;
@@ -303,6 +372,134 @@ function projectMaterialBinding(
303
372
  });
304
373
  }
305
374
 
375
+ function projectMessageSchemaRefNode(
376
+ node: ResourceNode,
377
+ resources: ReadonlyMap<string, ResourceRecord>,
378
+ location: string,
379
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
380
+ ): AIWorkflowMessageSchemaProjection | undefined {
381
+ const typedRef = projectTypedRefNode(node, resources, location, diagnostics, 'MessageSchema');
382
+ if (!typedRef) return undefined;
383
+ const schemaValue = ownResourceValue(typedRef.resource.node.properties, 'schema');
384
+ if (!isResourceValueMap(schemaValue)) {
385
+ diagnostics.push(diagnostic(
386
+ 'AI_MESSAGE_SCHEMA_VALUE_INVALID',
387
+ typedRef.resource.documentUri,
388
+ 'MessageSchema must declare one explicit schema map.',
389
+ ));
390
+ return undefined;
391
+ }
392
+ const schema = cloneResourceValue(schemaValue, typedRef.resource.documentUri, diagnostics, 'AI_MESSAGE_SCHEMA_VALUE_INVALID');
393
+ if (!isResourceValueMap(schema)) return undefined;
394
+ return Object.freeze({ ...typedRef, kind: 'MessageSchema', schema });
395
+ }
396
+
397
+ function projectMaterialValue(
398
+ typedRef: AIWorkflowTypedResourceRef,
399
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
400
+ ): AIWorkflowMaterialValueProjection | undefined {
401
+ const value = ownResourceValue(typedRef.resource.node.properties, 'value');
402
+ if (value === undefined) {
403
+ diagnostics.push(diagnostic(
404
+ 'AI_MATERIAL_VALUE_MISSING',
405
+ typedRef.resource.documentUri,
406
+ `Material '${typedRef.resource.resourceId}' must declare one explicit value.`,
407
+ ));
408
+ return undefined;
409
+ }
410
+ const cloned = cloneResourceValue(value, typedRef.resource.documentUri, diagnostics, 'AI_MATERIAL_VALUE_INVALID');
411
+ return cloned === undefined ? undefined : Object.freeze({ ...typedRef, value: cloned });
412
+ }
413
+
414
+ function ownResourceValue(
415
+ record: Readonly<Record<string, ResourceValue>>,
416
+ field: string,
417
+ ): ResourceValue | undefined {
418
+ const descriptor = Object.getOwnPropertyDescriptor(record, field);
419
+ return descriptor && descriptor.enumerable && 'value' in descriptor
420
+ ? descriptor.value
421
+ : undefined;
422
+ }
423
+
424
+ function isResourceValueMap(value: ResourceValue | undefined): value is Readonly<Record<string, ResourceValue>> {
425
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && !isResourceNode(value);
426
+ }
427
+
428
+ function cloneResourceValue(
429
+ value: ResourceValue,
430
+ location: string,
431
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
432
+ code: string,
433
+ ancestors: WeakSet<object> = new WeakSet(),
434
+ ): ResourceValue | undefined {
435
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
436
+ if (typeof value === 'number') {
437
+ if (Number.isFinite(value)) return value;
438
+ diagnostics.push(diagnostic(code, location, 'Executable resource values may contain only finite numbers.'));
439
+ return undefined;
440
+ }
441
+ if (ancestors.has(value)) {
442
+ diagnostics.push(diagnostic(code, location, 'Executable resource values must not contain cycles.'));
443
+ return undefined;
444
+ }
445
+ ancestors.add(value);
446
+ if (Array.isArray(value)) {
447
+ const keys = Reflect.ownKeys(value);
448
+ const expected = new Set([...value.keys()].map(String).concat('length'));
449
+ if (keys.some((key) => typeof key !== 'string' || !expected.has(key))) {
450
+ diagnostics.push(diagnostic(code, location, 'Executable resource arrays must be dense plain-data arrays.'));
451
+ ancestors.delete(value);
452
+ return undefined;
453
+ }
454
+ const output: ResourceValue[] = [];
455
+ for (let index = 0; index < value.length; index += 1) {
456
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
457
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
458
+ diagnostics.push(diagnostic(code, location, 'Executable resource arrays must be dense plain-data arrays.'));
459
+ ancestors.delete(value);
460
+ return undefined;
461
+ }
462
+ const item = cloneResourceValue(descriptor.value as ResourceValue, location, diagnostics, code, ancestors);
463
+ if (item === undefined) {
464
+ ancestors.delete(value);
465
+ return undefined;
466
+ }
467
+ output.push(item);
468
+ }
469
+ ancestors.delete(value);
470
+ return Object.freeze(output);
471
+ }
472
+ const prototype = Object.getPrototypeOf(value);
473
+ if (prototype !== Object.prototype && prototype !== null) {
474
+ diagnostics.push(diagnostic(code, location, 'Executable resource maps must be plain-data objects.'));
475
+ ancestors.delete(value);
476
+ return undefined;
477
+ }
478
+ const output: Record<string, ResourceValue> = Object.create(null) as Record<string, ResourceValue>;
479
+ const keys = Reflect.ownKeys(value);
480
+ if (keys.some((key) => typeof key !== 'string')) {
481
+ diagnostics.push(diagnostic(code, location, 'Executable resource maps may contain only string keys.'));
482
+ ancestors.delete(value);
483
+ return undefined;
484
+ }
485
+ for (const key of (keys as string[]).sort(compareCodeUnits)) {
486
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
487
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
488
+ diagnostics.push(diagnostic(code, location, 'Executable resource maps must contain only enumerable data properties.'));
489
+ ancestors.delete(value);
490
+ return undefined;
491
+ }
492
+ const item = cloneResourceValue(descriptor.value as ResourceValue, location, diagnostics, code, ancestors);
493
+ if (item === undefined) {
494
+ ancestors.delete(value);
495
+ return undefined;
496
+ }
497
+ output[key] = item;
498
+ }
499
+ ancestors.delete(value);
500
+ return Object.freeze(output);
501
+ }
502
+
306
503
  function projectTaskRef(
307
504
  node: ResourceNode,
308
505
  resources: ReadonlyMap<string, ResourceRecord>,
package/src/run-freeze.ts CHANGED
@@ -154,18 +154,38 @@ function semanticResourceFingerprint(
154
154
  id: message.id,
155
155
  role: message.role,
156
156
  prompt: [message.prompt.kind, message.prompt.resource.resourceId],
157
- schema: message.schema ? [message.schema.kind, message.schema.resource.resourceId] : null,
157
+ schema: message.schema ? {
158
+ kind: message.schema.kind,
159
+ resourceId: message.schema.resource.resourceId,
160
+ value: message.schema.schema,
161
+ } : null,
158
162
  })),
159
- inputSchema: agent.inputSchema ? [agent.inputSchema.kind, agent.inputSchema.resource.resourceId] : null,
160
- outputSchema: agent.outputSchema ? [agent.outputSchema.kind, agent.outputSchema.resource.resourceId] : null,
163
+ inputSchema: agent.inputSchema ? {
164
+ kind: agent.inputSchema.kind,
165
+ resourceId: agent.inputSchema.resource.resourceId,
166
+ value: agent.inputSchema.schema,
167
+ } : null,
168
+ outputSchema: agent.outputSchema ? {
169
+ kind: agent.outputSchema.kind,
170
+ resourceId: agent.outputSchema.resource.resourceId,
171
+ value: agent.outputSchema.schema,
172
+ } : null,
161
173
  tools: agent.tools.map((ref) => [ref.kind, ref.resource.resourceId]),
162
- effectPolicy: agent.effectPolicy ? [agent.effectPolicy.kind, agent.effectPolicy.resource.resourceId] : null,
174
+ effectPolicy: agent.effectPolicy ? {
175
+ kind: agent.effectPolicy.kind,
176
+ resourceId: agent.effectPolicy.resource.resourceId,
177
+ toolMode: agent.effectPolicy.toolMode,
178
+ } : null,
163
179
  materialPorts: agent.materialPorts.map((ref) => [ref.kind, ref.resource.resourceId]),
164
180
  },
165
181
  bindings: bindings.map((binding) => ({
166
182
  resourceId: binding.resource.resourceId,
167
183
  port: [binding.port.resource.resourceId, binding.port.materialKind, binding.port.required, binding.port.cardinality],
168
- material: [binding.material.kind, binding.material.resource.resourceId],
184
+ material: {
185
+ kind: binding.material.kind,
186
+ resourceId: binding.material.resource.resourceId,
187
+ value: binding.material.value,
188
+ },
169
189
  })),
170
190
  closure: closure.map((resource) => ({
171
191
  resourceId: resource.resourceId,