ai-workflow-logic 0.1.8 → 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.
@@ -0,0 +1,755 @@
1
+ import type {
2
+ AIAgentDefinitionAuthoringEvidence,
3
+ AIAgentDefinitionCandidate,
4
+ AIAgentDefinitionCandidateContract,
5
+ AIAgentDefinitionCandidateSet,
6
+ AIAgentDefinitionSelectionAdmission,
7
+ AIAgentDefinitionSelectionDecision,
8
+ AIAgentTaskRequirement,
9
+ AIAgentTaskRequirementProjection,
10
+ AuthorNewAIAgentDefinitionAdmission,
11
+ AIWorkflowAgentResourceProjection,
12
+ AIWorkflowDefinitionResourceKind,
13
+ FrozenAIAgentTaskBinding,
14
+ SelectedAIAgentDefinitionAdmission,
15
+ } from 'ai-workflow-contract';
16
+ import { AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION } from 'ai-workflow-contract';
17
+ import {
18
+ RESOURCE_AUTHORING_SCHEMA_VERSION,
19
+ deriveResourceAuthoringRegistryRevision,
20
+ type ResourceAuthoringProposal,
21
+ type ResourceAuthoringReceipt,
22
+ type ResourceAuthoringTransactionPort,
23
+ } from 'halfcode-compiler.xnl/authoring-runtime';
24
+ import {
25
+ resolveEffectiveResourceContentIdentities,
26
+ sha256Digest,
27
+ type EffectiveResourceRegistry,
28
+ type ResourceContentIdentity,
29
+ type ResourceLayerContentIdentityInput,
30
+ type ResourceOrigin,
31
+ type Sha256Digest,
32
+ } from 'halfcode-compiler.xnl/resource-core';
33
+ import { sourceRegistryForAIWorkflowAgentResourceProjection } from './agent-resource-brand.js';
34
+ import {
35
+ freezeAIWorkflowRunResources,
36
+ projectFrozenAIAgentTaskBinding,
37
+ } from './run-freeze.js';
38
+
39
+ export type AIAgentDefinitionSelectionErrorCode =
40
+ | 'AI_AGENT_REQUIREMENT_INVALID'
41
+ | 'AI_AGENT_CANDIDATE_PROJECTION_UNTRUSTED'
42
+ | 'AI_AGENT_CANDIDATE_CONTENT_IDENTITY_MISSING'
43
+ | 'AI_AGENT_AUTHORING_RECEIPT_INVALID'
44
+ | 'AI_AGENT_AUTHORING_RECEIPT_STALE'
45
+ | 'AI_AGENT_AUTHORING_RECEIPT_RESOURCE_MISMATCH'
46
+ | 'AI_AGENT_SELECTION_ADMISSION_UNTRUSTED';
47
+
48
+ export class AIAgentDefinitionSelectionError extends Error {
49
+ constructor(
50
+ public readonly code: AIAgentDefinitionSelectionErrorCode,
51
+ message: string,
52
+ ) {
53
+ super(message);
54
+ this.name = 'AIAgentDefinitionSelectionError';
55
+ }
56
+ }
57
+
58
+ export interface ProjectAIAgentDefinitionCandidatesInput {
59
+ readonly registry: EffectiveResourceRegistry;
60
+ readonly projection: AIWorkflowAgentResourceProjection;
61
+ readonly layers: readonly ResourceLayerContentIdentityInput[];
62
+ }
63
+
64
+ export interface ReconcileAuthoredAIAgentDefinitionInput
65
+ extends ProjectAIAgentDefinitionCandidatesInput {
66
+ readonly candidateSet: AIAgentDefinitionCandidateSet;
67
+ readonly authoringAdmission: AuthorNewAIAgentDefinitionAdmission;
68
+ readonly receipt: ResourceAuthoringReceipt;
69
+ readonly receiptAuthority: Pick<ResourceAuthoringTransactionPort, 'loadReceipt'>;
70
+ }
71
+
72
+ export interface AdmitAIAgentDefinitionSelectionInput {
73
+ readonly requirement: AIAgentTaskRequirementProjection;
74
+ readonly candidateSet: AIAgentDefinitionCandidateSet;
75
+ readonly decision: AIAgentDefinitionSelectionDecision;
76
+ }
77
+
78
+ export interface FreezeSelectedAIAgentTaskBindingInput {
79
+ readonly registry: EffectiveResourceRegistry;
80
+ readonly projection: AIWorkflowAgentResourceProjection;
81
+ readonly contentIdentities: ReadonlyMap<string, ResourceContentIdentity>;
82
+ readonly admission: SelectedAIAgentDefinitionAdmission;
83
+ readonly workflowKind: AIWorkflowDefinitionResourceKind;
84
+ readonly workflowRef: `resource://${string}`;
85
+ readonly nodeId: string;
86
+ }
87
+
88
+ interface CandidateSetAuthority {
89
+ readonly registry: EffectiveResourceRegistry;
90
+ readonly projection: AIWorkflowAgentResourceProjection;
91
+ readonly layers: readonly ResourceLayerContentIdentityInput[];
92
+ }
93
+
94
+ interface AuthoringAdmissionAuthority {
95
+ readonly candidateSetAuthority: CandidateSetAuthority;
96
+ readonly candidateSet: AIAgentDefinitionCandidateSet;
97
+ readonly requirement: AIAgentTaskRequirementProjection;
98
+ readonly proposalDigest: Sha256Digest;
99
+ }
100
+
101
+ const authenticRequirements = new WeakSet<object>();
102
+ const candidateSetAuthorities = new WeakMap<object, CandidateSetAuthority>();
103
+ const selectedAdmissionAuthorities = new WeakMap<object, CandidateSetAuthority>();
104
+ const authoringAdmissionAuthorities = new WeakMap<object, AuthoringAdmissionAuthority>();
105
+ const pendingAuthoringAdmissions = new Map<`resource://${string}`, object>();
106
+
107
+ export function normalizeAIAgentTaskRequirement(
108
+ value: AIAgentTaskRequirement,
109
+ ): AIAgentTaskRequirementProjection {
110
+ const input = closedRecord(value, 'requirement');
111
+ exactKeys(input, [
112
+ 'schemaVersion',
113
+ 'requirementId',
114
+ 'objective',
115
+ 'inputSchemaRef',
116
+ 'outputSchemaRef',
117
+ 'requiredToolRefs',
118
+ 'requiredEffectPolicyRef',
119
+ 'requiredToolMode',
120
+ 'requiredMaterialPortRefs',
121
+ 'requiredContextPipelineRef',
122
+ 'requiredMessageSourceRefs',
123
+ ], 'requirement');
124
+ if (input.schemaVersion !== AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION) {
125
+ invalidRequirement('requirement.schemaVersion is invalid.');
126
+ }
127
+ const requirementId = nonEmptyString(input.requirementId, 'requirement.requirementId');
128
+ const objective = stringValue(input.objective, 'requirement.objective');
129
+ const requiredToolMode = optionalString(input.requiredToolMode, 'requirement.requiredToolMode');
130
+ if (requiredToolMode !== undefined && requiredToolMode !== 'declared-only' && requiredToolMode !== 'none') {
131
+ invalidRequirement('requirement.requiredToolMode is invalid.');
132
+ }
133
+ const requirement = Object.freeze({
134
+ schemaVersion: AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION,
135
+ requirementId,
136
+ objective,
137
+ ...optionalRef('inputSchemaRef', input.inputSchemaRef, 'requirement.inputSchemaRef'),
138
+ ...optionalRef('outputSchemaRef', input.outputSchemaRef, 'requirement.outputSchemaRef'),
139
+ requiredToolRefs: exactRefSet(input.requiredToolRefs, 'requirement.requiredToolRefs'),
140
+ ...optionalRef('requiredEffectPolicyRef', input.requiredEffectPolicyRef, 'requirement.requiredEffectPolicyRef'),
141
+ ...(requiredToolMode === undefined ? {} : { requiredToolMode }),
142
+ requiredMaterialPortRefs: exactRefSet(input.requiredMaterialPortRefs, 'requirement.requiredMaterialPortRefs'),
143
+ ...optionalRef('requiredContextPipelineRef', input.requiredContextPipelineRef, 'requirement.requiredContextPipelineRef'),
144
+ requiredMessageSourceRefs: exactRefSet(input.requiredMessageSourceRefs, 'requirement.requiredMessageSourceRefs'),
145
+ }) satisfies AIAgentTaskRequirement;
146
+ const projection = Object.freeze({
147
+ schemaVersion: AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION,
148
+ requirement,
149
+ requirementDigest: digest({
150
+ schemaVersion: AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION,
151
+ requirement,
152
+ }),
153
+ }) satisfies AIAgentTaskRequirementProjection;
154
+ authenticRequirements.add(projection);
155
+ return projection;
156
+ }
157
+
158
+ export function projectAIAgentDefinitionCandidates(
159
+ input: ProjectAIAgentDefinitionCandidatesInput,
160
+ ): AIAgentDefinitionCandidateSet {
161
+ assertProjectionAuthority(input);
162
+ const contentIdentities = resolveEffectiveResourceContentIdentities({
163
+ registry: input.registry,
164
+ layers: input.layers,
165
+ });
166
+ const registryRevision = asDigest(deriveResourceAuthoringRegistryRevision({
167
+ registry: input.registry,
168
+ layers: input.layers,
169
+ }), 'registry revision');
170
+ const candidates = input.projection.agentDefinitions
171
+ .map((agent): AIAgentDefinitionCandidate => {
172
+ const identity = contentIdentities.get(agent.resource.resourceId);
173
+ if (!identity) {
174
+ throw new AIAgentDefinitionSelectionError(
175
+ 'AI_AGENT_CANDIDATE_CONTENT_IDENTITY_MISSING',
176
+ `AIAgentDefinition '${agent.resource.resourceId}' has no effective content identity.`,
177
+ );
178
+ }
179
+ const contract = Object.freeze({
180
+ ...(agent.inputSchema === undefined ? {} : { inputSchemaRef: agent.inputSchema.ref }),
181
+ ...(agent.outputSchema === undefined ? {} : { outputSchemaRef: agent.outputSchema.ref }),
182
+ toolRefs: sortedUnique(agent.tools.map(({ ref }) => ref)),
183
+ ...(agent.effectPolicy === undefined ? {} : {
184
+ effectPolicyRef: agent.effectPolicy.ref,
185
+ effectToolMode: agent.effectPolicy.toolMode,
186
+ }),
187
+ materialPortRefs: sortedUnique(agent.materialPorts.map(({ ref }) => ref)),
188
+ ...(agent.contextPipeline === undefined ? {} : { contextPipelineRef: agent.contextPipeline.ref }),
189
+ messageSourceRefs: sortedUnique(agent.messagePrefix
190
+ .filter((item) => item.type === 'message-source')
191
+ .map(({ source }) => source.ref)),
192
+ }) satisfies AIAgentDefinitionCandidateContract;
193
+ const unsigned = Object.freeze({
194
+ schemaVersion: AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION,
195
+ agentDefinitionRef: resourceRef(agent.resource.resourceId),
196
+ contentDigest: identity.contentDigest,
197
+ registryRevision,
198
+ contract,
199
+ });
200
+ return Object.freeze({
201
+ ...unsigned,
202
+ candidateDigest: digest(unsigned),
203
+ });
204
+ })
205
+ .sort((left, right) => compareCodeUnits(left.agentDefinitionRef, right.agentDefinitionRef));
206
+ const candidateSet = Object.freeze({
207
+ schemaVersion: AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION,
208
+ registryRevision,
209
+ candidates: Object.freeze(candidates),
210
+ candidateSetDigest: candidateSetDigest(registryRevision, candidates),
211
+ }) satisfies AIAgentDefinitionCandidateSet;
212
+ candidateSetAuthorities.set(candidateSet, Object.freeze({
213
+ registry: input.registry,
214
+ projection: input.projection,
215
+ layers: input.layers,
216
+ }));
217
+ return candidateSet;
218
+ }
219
+
220
+ export async function reconcileAuthoredAIAgentDefinition(
221
+ input: ReconcileAuthoredAIAgentDefinitionInput,
222
+ ): Promise<AIAgentDefinitionCandidateSet> {
223
+ const authoringAuthority = authoringAdmissionAuthorities.get(input.authoringAdmission as object);
224
+ const authority = candidateSetAuthorities.get(input.candidateSet);
225
+ if (!authority
226
+ || authority.registry !== input.registry
227
+ || authority.projection !== input.projection
228
+ || authority.layers !== input.layers
229
+ || !authoringAuthority
230
+ || candidateSetAuthorities.get(authoringAuthority.candidateSet)
231
+ !== authoringAuthority.candidateSetAuthority
232
+ || authoringAuthority.requirement.requirementDigest
233
+ !== input.authoringAdmission.requirementDigest
234
+ || pendingAuthoringAdmissions.get(input.authoringAdmission.agentDefinitionRef)
235
+ !== input.authoringAdmission
236
+ || authoringAuthority.proposalDigest !== digest(input.authoringAdmission.proposal)) {
237
+ throw new AIAgentDefinitionSelectionError(
238
+ 'AI_AGENT_CANDIDATE_PROJECTION_UNTRUSTED',
239
+ 'Authoring reconciliation requires the exact author-new admission and candidate projection for this registry observation.',
240
+ );
241
+ }
242
+ validateReceiptShapeAndDigest(input.receipt);
243
+ const durableReceipt = await input.receiptAuthority.loadReceipt(input.receipt.planDigest).catch(() => undefined);
244
+ if (!durableReceipt || canonicalJson(durableReceipt) !== canonicalJson(input.receipt)) {
245
+ throw new AIAgentDefinitionSelectionError(
246
+ 'AI_AGENT_AUTHORING_RECEIPT_INVALID',
247
+ 'The supplied receipt is not the immutable receipt loaded from the Halfcode transaction authority.',
248
+ );
249
+ }
250
+ validateReceiptShapeAndDigest(durableReceipt);
251
+ const proposal = input.authoringAdmission.proposal;
252
+ const currentRevision = asDigest(deriveResourceAuthoringRegistryRevision({
253
+ registry: input.registry,
254
+ layers: input.layers,
255
+ }), 'current registry revision');
256
+ if (input.receipt.registryRevisionAfter !== currentRevision
257
+ || input.candidateSet.registryRevision !== currentRevision) {
258
+ throw new AIAgentDefinitionSelectionError(
259
+ 'AI_AGENT_AUTHORING_RECEIPT_STALE',
260
+ 'The authoring receipt does not describe the current effective registry revision.',
261
+ );
262
+ }
263
+ const entry = input.registry.byId.get(input.receipt.resourceId);
264
+ const resource = entry?.resource;
265
+ const identities = resolveEffectiveResourceContentIdentities({
266
+ registry: input.registry,
267
+ layers: input.layers,
268
+ });
269
+ const identity = identities.get(input.receipt.resourceId);
270
+ const effectiveOrigin = entry?.effectiveOrigin;
271
+ const authorityResourceIds = [...input.registry.byId.values()]
272
+ .filter((value) => value.resource && sameOrigin(value.effectiveOrigin, effectiveOrigin))
273
+ .map(({ resourceId }) => resourceId)
274
+ .sort(compareCodeUnits);
275
+ const candidate = input.candidateSet.candidates.find(({ agentDefinitionRef }) =>
276
+ agentDefinitionRef === resourceRef(input.receipt.resourceId));
277
+ if (!resource
278
+ || resource.kind !== 'AIAgentDefinition'
279
+ || input.authoringAdmission.agentDefinitionRef !== resourceRef(input.receipt.resourceId)
280
+ || input.receipt.operation !== proposal.operation
281
+ || input.receipt.catalogId !== proposal.catalogId
282
+ || input.receipt.resourceId !== proposal.resourceId
283
+ || input.receipt.kind !== proposal.kind
284
+ || input.receipt.apiVersion !== proposal.apiVersion
285
+ || input.receipt.sourceShape !== proposal.sourceShape
286
+ || input.receipt.documentUri !== proposal.documentUri
287
+ || input.receipt.authorityDigestBefore !== null
288
+ || input.receipt.authorityDigestAfter !== sha256Digest(proposal.authorityText)
289
+ || input.receipt.registryRevisionBefore !== proposal.expected.registryRevision
290
+ || input.receipt.kind !== 'AIAgentDefinition'
291
+ || resource.metadata.apiVersion !== input.receipt.apiVersion
292
+ || resource.sourceShape !== input.receipt.sourceShape
293
+ || resource.documentUri !== input.receipt.documentUri
294
+ || !identity
295
+ || identity.authorityDigest !== input.receipt.authorityDigestAfter
296
+ || identity.contentDigest !== input.receipt.contentDigest
297
+ || !effectiveOrigin
298
+ || !sameReceiptOrigin(effectiveOrigin, input.receipt.effectiveOrigin)
299
+ || canonicalJson(authorityResourceIds) !== canonicalJson([...input.receipt.authorityResourceIds].sort(compareCodeUnits))
300
+ || !candidate
301
+ || candidate.contentDigest !== input.receipt.contentDigest) {
302
+ throw new AIAgentDefinitionSelectionError(
303
+ 'AI_AGENT_AUTHORING_RECEIPT_RESOURCE_MISMATCH',
304
+ 'The authoring receipt does not exactly match the current AIAgentDefinition content, origin, authority unit, and candidate.',
305
+ );
306
+ }
307
+ const evidence = Object.freeze({
308
+ schemaVersion: 'ai-workflow.agent-definition-authoring-evidence/v1' as const,
309
+ planDigest: asDigest(input.receipt.planDigest, 'receipt plan digest'),
310
+ transactionId: input.receipt.transactionId,
311
+ receiptDigest: asDigest(input.receipt.receiptDigest, 'receipt digest'),
312
+ }) satisfies AIAgentDefinitionAuthoringEvidence;
313
+ const candidates = Object.freeze(input.candidateSet.candidates.map((value) => {
314
+ if (value !== candidate) return value;
315
+ const unsigned = Object.freeze({
316
+ schemaVersion: value.schemaVersion,
317
+ agentDefinitionRef: value.agentDefinitionRef,
318
+ contentDigest: value.contentDigest,
319
+ registryRevision: value.registryRevision,
320
+ contract: value.contract,
321
+ authoringEvidence: evidence,
322
+ });
323
+ return Object.freeze({ ...unsigned, candidateDigest: digest(unsigned) });
324
+ }));
325
+ const reconciled = Object.freeze({
326
+ ...input.candidateSet,
327
+ candidates,
328
+ candidateSetDigest: candidateSetDigest(input.candidateSet.registryRevision, candidates),
329
+ }) satisfies AIAgentDefinitionCandidateSet;
330
+ candidateSetAuthorities.set(reconciled, authority);
331
+ pendingAuthoringAdmissions.delete(input.authoringAdmission.agentDefinitionRef);
332
+ return reconciled;
333
+ }
334
+
335
+ export function admitAIAgentDefinitionSelection(
336
+ input: AdmitAIAgentDefinitionSelectionInput,
337
+ ): AIAgentDefinitionSelectionAdmission {
338
+ if (!isValidDecision(input.decision)) {
339
+ return rejection('decision-invalid', 'The selection decision is not one closed, canonical decision value.');
340
+ }
341
+ if (!authenticRequirements.has(input.requirement as object)
342
+ || input.decision.requirementDigest !== input.requirement.requirementDigest) {
343
+ return rejection('requirement-stale', 'The decision is not bound to the current normalized task requirement.');
344
+ }
345
+ const candidateSetAuthority = candidateSetAuthorities.get(input.candidateSet);
346
+ if (!candidateSetAuthority
347
+ || input.decision.candidateSetDigest !== input.candidateSet.candidateSetDigest) {
348
+ return rejection('candidate-set-stale', 'The decision is not bound to the current authentic candidate set.');
349
+ }
350
+ if (input.decision.mode === 'select-existing') {
351
+ const decision = input.decision;
352
+ const candidate = input.candidateSet.candidates.find(({ agentDefinitionRef }) =>
353
+ agentDefinitionRef === decision.candidateRef);
354
+ if (!candidate) return rejection('candidate-missing', 'The selected candidate is absent from the current set.');
355
+ if (pendingAuthoringAdmissions.has(candidate.agentDefinitionRef)
356
+ && candidate.authoringEvidence === undefined) {
357
+ return rejection(
358
+ 'authoring-in-progress',
359
+ 'An in-flight author-new transition must reconcile its durable Halfcode receipt before this definition is selectable.',
360
+ );
361
+ }
362
+ if (candidate.candidateDigest !== decision.candidateDigest) {
363
+ return rejection('candidate-digest-mismatch', 'The selected candidate digest is stale or forged.');
364
+ }
365
+ if (!isCompatible(input.requirement.requirement, candidate.contract)) {
366
+ return rejection('candidate-incompatible', 'The selected candidate does not satisfy the typed task requirement.');
367
+ }
368
+ const admission = Object.freeze({
369
+ schemaVersion: AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION,
370
+ status: 'selected' as const,
371
+ requirementDigest: input.requirement.requirementDigest,
372
+ candidateSetDigest: input.candidateSet.candidateSetDigest,
373
+ candidate,
374
+ reason: stringValue(decision.reason, 'decision.reason'),
375
+ }) satisfies SelectedAIAgentDefinitionAdmission;
376
+ selectedAdmissionAuthorities.set(admission, candidateSetAuthority);
377
+ return admission;
378
+ }
379
+ if (!isValidAuthoringProposal(input.decision.proposal, input.decision.agentDefinitionRef, input.candidateSet)) {
380
+ return rejection('authoring-proposal-invalid', 'The author-new decision is not one exact Halfcode create proposal for the current registry.');
381
+ }
382
+ const proposal = cloneAuthoringProposal(input.decision.proposal);
383
+ const admission = Object.freeze({
384
+ schemaVersion: AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION,
385
+ status: 'authoring-request' as const,
386
+ requirementDigest: input.requirement.requirementDigest,
387
+ candidateSetDigest: input.candidateSet.candidateSetDigest,
388
+ agentDefinitionRef: input.decision.agentDefinitionRef,
389
+ proposal,
390
+ reason: stringValue(input.decision.reason, 'decision.reason'),
391
+ }) satisfies AuthorNewAIAgentDefinitionAdmission;
392
+ if (pendingAuthoringAdmissions.has(admission.agentDefinitionRef)) {
393
+ return rejection('authoring-in-progress', 'An authoring request for this exact Agent definition is already in flight.');
394
+ }
395
+ authoringAdmissionAuthorities.set(admission, Object.freeze({
396
+ candidateSetAuthority,
397
+ candidateSet: input.candidateSet,
398
+ requirement: input.requirement,
399
+ proposalDigest: digest(proposal),
400
+ }));
401
+ pendingAuthoringAdmissions.set(admission.agentDefinitionRef, admission);
402
+ return admission;
403
+ }
404
+
405
+ export function freezeSelectedAIAgentTaskBinding(
406
+ input: FreezeSelectedAIAgentTaskBindingInput,
407
+ ): FrozenAIAgentTaskBinding {
408
+ const authority = selectedAdmissionAuthorities.get(input.admission as object);
409
+ if (!authority
410
+ || authority.registry !== input.registry
411
+ || authority.projection !== input.projection
412
+ || input.contentIdentities.get(resourceId(input.admission.candidate.agentDefinitionRef))?.contentDigest
413
+ !== input.admission.candidate.contentDigest) {
414
+ throw new AIAgentDefinitionSelectionError(
415
+ 'AI_AGENT_SELECTION_ADMISSION_UNTRUSTED',
416
+ 'Selected task freeze requires the exact admission produced by typed selection.',
417
+ );
418
+ }
419
+ return projectFrozenAIAgentTaskBinding(freezeAIWorkflowRunResources({
420
+ registry: input.registry,
421
+ projection: input.projection,
422
+ task: {
423
+ workflowKind: input.workflowKind,
424
+ workflowRef: input.workflowRef,
425
+ nodeId: nonEmptyString(input.nodeId, 'task.nodeId'),
426
+ agentDefinitionRef: input.admission.candidate.agentDefinitionRef,
427
+ },
428
+ contentIdentities: input.contentIdentities,
429
+ }));
430
+ }
431
+
432
+ function isValidDecision(value: AIAgentDefinitionSelectionDecision): boolean {
433
+ try {
434
+ const decision = closedRecord(value, 'decision');
435
+ if (decision.schemaVersion !== AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION
436
+ || !isDigest(decision.requirementDigest)
437
+ || !isDigest(decision.candidateSetDigest)) return false;
438
+ stringValue(decision.reason, 'decision.reason');
439
+ if (decision.mode === 'select-existing') {
440
+ exactKeys(decision, [
441
+ 'schemaVersion',
442
+ 'mode',
443
+ 'requirementDigest',
444
+ 'candidateSetDigest',
445
+ 'candidateRef',
446
+ 'candidateDigest',
447
+ 'reason',
448
+ ], 'decision');
449
+ exactRef(decision.candidateRef, 'decision.candidateRef');
450
+ return isDigest(decision.candidateDigest);
451
+ }
452
+ if (decision.mode === 'author-new') {
453
+ exactKeys(decision, [
454
+ 'schemaVersion',
455
+ 'mode',
456
+ 'requirementDigest',
457
+ 'candidateSetDigest',
458
+ 'agentDefinitionRef',
459
+ 'proposal',
460
+ 'reason',
461
+ ], 'decision');
462
+ exactRef(decision.agentDefinitionRef, 'decision.agentDefinitionRef');
463
+ return true;
464
+ }
465
+ return false;
466
+ } catch {
467
+ return false;
468
+ }
469
+ }
470
+
471
+ function assertProjectionAuthority(input: ProjectAIAgentDefinitionCandidatesInput): void {
472
+ if (sourceRegistryForAIWorkflowAgentResourceProjection(input.projection) !== input.registry) {
473
+ throw new AIAgentDefinitionSelectionError(
474
+ 'AI_AGENT_CANDIDATE_PROJECTION_UNTRUSTED',
475
+ 'Candidate projection requires the exact immutable Agent resource projection produced for this registry.',
476
+ );
477
+ }
478
+ }
479
+
480
+ function isCompatible(
481
+ requirement: AIAgentTaskRequirement,
482
+ candidate: AIAgentDefinitionCandidateContract,
483
+ ): boolean {
484
+ return optionalExact(requirement.inputSchemaRef, candidate.inputSchemaRef)
485
+ && optionalExact(requirement.outputSchemaRef, candidate.outputSchemaRef)
486
+ && containsAll(candidate.toolRefs, requirement.requiredToolRefs)
487
+ && optionalExact(requirement.requiredEffectPolicyRef, candidate.effectPolicyRef)
488
+ && optionalExact(requirement.requiredToolMode, candidate.effectToolMode)
489
+ && containsAll(candidate.materialPortRefs, requirement.requiredMaterialPortRefs)
490
+ && optionalExact(requirement.requiredContextPipelineRef, candidate.contextPipelineRef)
491
+ && containsAll(candidate.messageSourceRefs, requirement.requiredMessageSourceRefs);
492
+ }
493
+
494
+ function isValidAuthoringProposal(
495
+ value: ResourceAuthoringProposal,
496
+ definitionRef: `resource://${string}`,
497
+ candidateSet: AIAgentDefinitionCandidateSet,
498
+ ): boolean {
499
+ try {
500
+ const proposal = closedRecord(value, 'proposal');
501
+ exactKeys(proposal, [
502
+ 'schemaVersion',
503
+ 'operation',
504
+ 'catalogId',
505
+ 'resourceId',
506
+ 'kind',
507
+ 'apiVersion',
508
+ 'sourceShape',
509
+ 'documentUri',
510
+ 'authorityText',
511
+ 'expected',
512
+ ], 'proposal');
513
+ const expected = closedRecord(proposal.expected, 'proposal.expected');
514
+ exactKeys(expected, ['state', 'registryRevision'], 'proposal.expected');
515
+ return proposal.schemaVersion === RESOURCE_AUTHORING_SCHEMA_VERSION
516
+ && proposal.operation === 'create'
517
+ && proposal.kind === 'AIAgentDefinition'
518
+ && proposal.sourceShape === 'single-file'
519
+ && expected.state === 'absent'
520
+ && expected.registryRevision === candidateSet.registryRevision
521
+ && definitionRef === resourceRef(nonEmptyString(proposal.resourceId, 'proposal.resourceId'))
522
+ && !candidateSet.candidates.some(({ agentDefinitionRef }) => agentDefinitionRef === definitionRef)
523
+ && nonEmptyString(proposal.catalogId, 'proposal.catalogId').length > 0
524
+ && nonEmptyString(proposal.apiVersion, 'proposal.apiVersion').length > 0
525
+ && /^vfs:\/\/@\/.+\.xnl$/u.test(nonEmptyString(proposal.documentUri, 'proposal.documentUri'))
526
+ && stringValue(proposal.authorityText, 'proposal.authorityText').length > 0;
527
+ } catch {
528
+ return false;
529
+ }
530
+ }
531
+
532
+ function cloneAuthoringProposal(value: ResourceAuthoringProposal): ResourceAuthoringProposal {
533
+ return Object.freeze({
534
+ schemaVersion: value.schemaVersion,
535
+ operation: value.operation,
536
+ catalogId: value.catalogId,
537
+ resourceId: value.resourceId,
538
+ kind: value.kind,
539
+ apiVersion: value.apiVersion,
540
+ sourceShape: value.sourceShape,
541
+ documentUri: value.documentUri,
542
+ authorityText: value.authorityText,
543
+ expected: Object.freeze({ ...value.expected }),
544
+ });
545
+ }
546
+
547
+ function validateReceiptShapeAndDigest(receiptValue: ResourceAuthoringReceipt): void {
548
+ try {
549
+ const receipt = closedRecord(receiptValue, 'receipt');
550
+ exactKeys(receipt, [
551
+ 'schemaVersion',
552
+ 'planDigest',
553
+ 'transactionId',
554
+ 'operation',
555
+ 'catalogId',
556
+ 'resourceId',
557
+ 'kind',
558
+ 'apiVersion',
559
+ 'sourceShape',
560
+ 'documentUri',
561
+ 'authorityDigestBefore',
562
+ 'authorityDigestAfter',
563
+ 'registryRevisionBefore',
564
+ 'registryRevisionAfter',
565
+ 'contentDigest',
566
+ 'effectiveOrigin',
567
+ 'authorityResourceIds',
568
+ 'receiptDigest',
569
+ ], 'receipt');
570
+ const origin = closedRecord(receipt.effectiveOrigin, 'receipt.effectiveOrigin');
571
+ exactKeys(origin, ['layerId', 'layerIndex', 'packageId', 'documentUri', 'logicalPath'], 'receipt.effectiveOrigin');
572
+ if (receipt.schemaVersion !== RESOURCE_AUTHORING_SCHEMA_VERSION
573
+ || (receipt.operation !== 'create' && receipt.operation !== 'update')
574
+ || receipt.sourceShape !== 'single-file'
575
+ || (receipt.authorityDigestBefore !== null && !isDigest(receipt.authorityDigestBefore))
576
+ || !isDigest(receipt.planDigest)
577
+ || !isDigest(receipt.authorityDigestAfter)
578
+ || !isDigest(receipt.registryRevisionBefore)
579
+ || !isDigest(receipt.registryRevisionAfter)
580
+ || !isDigest(receipt.contentDigest)
581
+ || !isDigest(receipt.receiptDigest)
582
+ || !Array.isArray(receipt.authorityResourceIds)
583
+ || receipt.authorityResourceIds.some((value) => typeof value !== 'string' || value.length === 0)
584
+ || new Set(receipt.authorityResourceIds).size !== receipt.authorityResourceIds.length
585
+ || canonicalJson(receipt.authorityResourceIds) !== canonicalJson([...receipt.authorityResourceIds].sort(compareCodeUnits))
586
+ || !Number.isSafeInteger(origin.layerIndex)
587
+ || (origin.logicalPath !== undefined && typeof origin.logicalPath !== 'string')) {
588
+ throw new Error('invalid receipt fields');
589
+ }
590
+ for (const field of ['transactionId', 'catalogId', 'resourceId', 'kind', 'apiVersion', 'documentUri'] as const) {
591
+ nonEmptyString(receipt[field], `receipt.${field}`);
592
+ }
593
+ for (const field of ['layerId', 'packageId', 'documentUri'] as const) {
594
+ nonEmptyString(origin[field], `receipt.effectiveOrigin.${field}`);
595
+ }
596
+ const { receiptDigest, ...unsigned } = receipt;
597
+ if (receiptDigest !== digest(unsigned)) throw new Error('receipt digest mismatch');
598
+ } catch (cause) {
599
+ if (cause instanceof AIAgentDefinitionSelectionError) throw cause;
600
+ throw new AIAgentDefinitionSelectionError(
601
+ 'AI_AGENT_AUTHORING_RECEIPT_INVALID',
602
+ 'The Halfcode authoring receipt is malformed or its content digest is forged.',
603
+ );
604
+ }
605
+ }
606
+
607
+ function candidateSetDigest(
608
+ registryRevision: Sha256Digest,
609
+ candidates: readonly AIAgentDefinitionCandidate[],
610
+ ): Sha256Digest {
611
+ return digest({
612
+ schemaVersion: AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION,
613
+ registryRevision,
614
+ candidates: candidates.map(({ agentDefinitionRef, candidateDigest }) => ({
615
+ agentDefinitionRef,
616
+ candidateDigest,
617
+ })),
618
+ });
619
+ }
620
+
621
+ function rejection(
622
+ code: Extract<AIAgentDefinitionSelectionAdmission, { status: 'rejected' }>['code'],
623
+ message: string,
624
+ ): AIAgentDefinitionSelectionAdmission {
625
+ return Object.freeze({
626
+ schemaVersion: AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION,
627
+ status: 'rejected' as const,
628
+ code,
629
+ message,
630
+ });
631
+ }
632
+
633
+ function sameOrigin(
634
+ left: ResourceOrigin | undefined,
635
+ right: ResourceOrigin | undefined,
636
+ ): boolean {
637
+ if (!left || !right) return false;
638
+ return canonicalJson(left) === canonicalJson(right);
639
+ }
640
+
641
+ function sameReceiptOrigin(
642
+ effective: ResourceOrigin,
643
+ receipt: ResourceAuthoringReceipt['effectiveOrigin'],
644
+ ): boolean {
645
+ return canonicalJson(effective) === canonicalJson(receipt);
646
+ }
647
+
648
+ function containsAll(haystack: readonly string[], needles: readonly string[]): boolean {
649
+ const available = new Set(haystack);
650
+ return needles.every((value) => available.has(value));
651
+ }
652
+
653
+ function optionalExact<T>(required: T | undefined, actual: T | undefined): boolean {
654
+ return required === undefined || required === actual;
655
+ }
656
+
657
+ function optionalRef<K extends string>(
658
+ key: K,
659
+ value: unknown,
660
+ location: string,
661
+ ): { readonly [P in K]?: `resource://${string}` } {
662
+ return value === undefined ? {} : { [key]: exactRef(value, location) } as { readonly [P in K]: `resource://${string}` };
663
+ }
664
+
665
+ function exactRefSet(value: unknown, location: string): readonly `resource://${string}`[] {
666
+ if (!Array.isArray(value)) invalidRequirement(`${location} must be an array.`);
667
+ return sortedUnique(value.map((item, index) => exactRef(item, `${location}[${index}]`)));
668
+ }
669
+
670
+ function exactRef(value: unknown, location: string): `resource://${string}` {
671
+ const text = nonEmptyString(value, location);
672
+ if (!/^resource:\/\/[^\s/][^\s]*$/u.test(text)) invalidRequirement(`${location} must be one exact resource:// reference.`);
673
+ return text as `resource://${string}`;
674
+ }
675
+
676
+ function resourceRef(resourceId: string): `resource://${string}` {
677
+ return `resource://${resourceId}`;
678
+ }
679
+
680
+ function resourceId(ref: `resource://${string}`): string {
681
+ return ref.slice('resource://'.length);
682
+ }
683
+
684
+ function sortedUnique<T extends string>(values: readonly T[]): readonly T[] {
685
+ return Object.freeze([...new Set(values)].sort(compareCodeUnits));
686
+ }
687
+
688
+ function closedRecord(value: unknown, location: string): Record<string, unknown> {
689
+ if (typeof value !== 'object' || value === null || Array.isArray(value)
690
+ || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
691
+ || Object.getOwnPropertySymbols(value).length > 0) {
692
+ throw new Error(`${location} must be closed own-data.`);
693
+ }
694
+ for (const key of Object.keys(value)) {
695
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
696
+ if (!descriptor || !('value' in descriptor)) throw new Error(`${location}.${key} must be a data property.`);
697
+ }
698
+ return value as Record<string, unknown>;
699
+ }
700
+
701
+ function exactKeys(value: Record<string, unknown>, allowed: readonly string[], location: string): void {
702
+ const extra = Object.keys(value).filter((key) => !allowed.includes(key));
703
+ if (extra.length > 0) throw new Error(`${location}.${extra.sort(compareCodeUnits)[0]} is unsupported.`);
704
+ }
705
+
706
+ function stringValue(value: unknown, location: string): string {
707
+ if (typeof value !== 'string') throw new Error(`${location} must be a string.`);
708
+ return value;
709
+ }
710
+
711
+ function optionalString(value: unknown, location: string): string | undefined {
712
+ return value === undefined ? undefined : stringValue(value, location);
713
+ }
714
+
715
+ function nonEmptyString(value: unknown, location: string): string {
716
+ const text = stringValue(value, location);
717
+ if (text.length === 0 || text.trim() !== text) throw new Error(`${location} must be a canonical non-empty string.`);
718
+ return text;
719
+ }
720
+
721
+ function invalidRequirement(message: string): never {
722
+ throw new AIAgentDefinitionSelectionError('AI_AGENT_REQUIREMENT_INVALID', message);
723
+ }
724
+
725
+ function asDigest(value: string, location: string): Sha256Digest {
726
+ if (!isDigest(value)) throw new Error(`${location} is not a sha256 digest.`);
727
+ return value;
728
+ }
729
+
730
+ function isDigest(value: unknown): value is Sha256Digest {
731
+ return typeof value === 'string' && /^sha256:[0-9a-f]{64}$/u.test(value);
732
+ }
733
+
734
+ function digest(value: unknown): Sha256Digest {
735
+ return sha256Digest(canonicalJson(value));
736
+ }
737
+
738
+ function canonicalJson(value: unknown): string {
739
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value);
740
+ if (typeof value === 'number') {
741
+ if (!Number.isFinite(value)) throw new Error('Canonical JSON cannot encode non-finite numbers.');
742
+ return JSON.stringify(value);
743
+ }
744
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
745
+ const record = closedRecord(value, 'canonical value');
746
+ return `{${Object.keys(record).sort(compareCodeUnits).map((key) => {
747
+ const item = record[key];
748
+ if (item === undefined) throw new Error('Canonical JSON cannot encode undefined.');
749
+ return `${JSON.stringify(key)}:${canonicalJson(item)}`;
750
+ }).join(',')}}`;
751
+ }
752
+
753
+ function compareCodeUnits(left: string, right: string): number {
754
+ return left < right ? -1 : left > right ? 1 : 0;
755
+ }