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.
- package/package.json +3 -3
- package/src/agent-definition-selection.ts +755 -0
- package/src/agent-invocation.ts +2 -2
- package/src/agent-resources.ts +134 -29
- package/src/agent-state.ts +5 -5
- package/src/checkpoint-store.ts +3 -1
- package/src/index.ts +15 -0
- package/src/run-checkpoint.ts +225 -3
- package/src/run-freeze.ts +20 -10
|
@@ -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
|
+
}
|