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.
- package/package.json +8 -6
- package/src/agent-code-brand.ts +14 -0
- package/src/agent-code-execution.ts +98 -0
- package/src/agent-code-projection.ts +98 -0
- package/src/agent-definition-selection.ts +274 -24
- package/src/agent-execution-resources.ts +160 -0
- package/src/agent-resources.ts +3 -0
- package/src/browser.ts +1 -1
- package/src/core.ts +563 -0
- package/src/filesystem.ts +22 -2
- package/src/index.ts +6 -563
- package/src/resource-resolution.ts +145 -0
- package/src/run-freeze.ts +90 -6
|
@@ -12,6 +12,9 @@ import type {
|
|
|
12
12
|
AIWorkflowDefinitionResourceKind,
|
|
13
13
|
FrozenAIAgentTaskBinding,
|
|
14
14
|
SelectedAIAgentDefinitionAdmission,
|
|
15
|
+
CompiledAIAgentCodeExecution,
|
|
16
|
+
ReviseExistingAIAgentDefinitionAdmission,
|
|
17
|
+
ReviseExistingAIAgentDefinitionDecision,
|
|
15
18
|
} from 'ai-workflow-contract';
|
|
16
19
|
import { AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION } from 'ai-workflow-contract';
|
|
17
20
|
import {
|
|
@@ -22,6 +25,7 @@ import {
|
|
|
22
25
|
type ResourceAuthoringTransactionPort,
|
|
23
26
|
} from 'halfcode-compiler.xnl/authoring-runtime';
|
|
24
27
|
import {
|
|
28
|
+
RESOURCE_ENVELOPE_VERSION,
|
|
25
29
|
resolveEffectiveResourceContentIdentities,
|
|
26
30
|
sha256Digest,
|
|
27
31
|
type EffectiveResourceRegistry,
|
|
@@ -31,8 +35,10 @@ import {
|
|
|
31
35
|
type Sha256Digest,
|
|
32
36
|
} from 'halfcode-compiler.xnl/resource-core';
|
|
33
37
|
import { sourceRegistryForAIWorkflowAgentResourceProjection } from './agent-resource-brand.js';
|
|
38
|
+
import { assertAIAgentExecutionResources, type AIAgentExecutionResourceObservation } from './agent-execution-resources.js';
|
|
34
39
|
import {
|
|
35
40
|
freezeAIWorkflowRunResources,
|
|
41
|
+
frozenAIAgentTaskAgentContentDigest,
|
|
36
42
|
projectFrozenAIAgentTaskBinding,
|
|
37
43
|
} from './run-freeze.js';
|
|
38
44
|
|
|
@@ -40,6 +46,7 @@ export type AIAgentDefinitionSelectionErrorCode =
|
|
|
40
46
|
| 'AI_AGENT_REQUIREMENT_INVALID'
|
|
41
47
|
| 'AI_AGENT_CANDIDATE_PROJECTION_UNTRUSTED'
|
|
42
48
|
| 'AI_AGENT_CANDIDATE_CONTENT_IDENTITY_MISSING'
|
|
49
|
+
| 'AI_AGENT_SELECTION_OWNER_MISMATCH'
|
|
43
50
|
| 'AI_AGENT_AUTHORING_RECEIPT_INVALID'
|
|
44
51
|
| 'AI_AGENT_AUTHORING_RECEIPT_STALE'
|
|
45
52
|
| 'AI_AGENT_AUTHORING_RECEIPT_RESOURCE_MISMATCH'
|
|
@@ -56,6 +63,9 @@ export class AIAgentDefinitionSelectionError extends Error {
|
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
export interface ProjectAIAgentDefinitionCandidatesInput {
|
|
66
|
+
readonly executionResources?: AIAgentExecutionResourceObservation;
|
|
67
|
+
/** Reuse one owner for all observations of the same resource host. */
|
|
68
|
+
readonly selectionOwner?: AIAgentDefinitionSelectionOwner;
|
|
59
69
|
readonly registry: EffectiveResourceRegistry;
|
|
60
70
|
readonly projection: AIWorkflowAgentResourceProjection;
|
|
61
71
|
readonly layers: readonly ResourceLayerContentIdentityInput[];
|
|
@@ -64,18 +74,24 @@ export interface ProjectAIAgentDefinitionCandidatesInput {
|
|
|
64
74
|
export interface ReconcileAuthoredAIAgentDefinitionInput
|
|
65
75
|
extends ProjectAIAgentDefinitionCandidatesInput {
|
|
66
76
|
readonly candidateSet: AIAgentDefinitionCandidateSet;
|
|
67
|
-
readonly authoringAdmission: AuthorNewAIAgentDefinitionAdmission;
|
|
77
|
+
readonly authoringAdmission: AuthorNewAIAgentDefinitionAdmission | ReviseExistingAIAgentDefinitionAdmission;
|
|
68
78
|
readonly receipt: ResourceAuthoringReceipt;
|
|
69
79
|
readonly receiptAuthority: Pick<ResourceAuthoringTransactionPort, 'loadReceipt'>;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
export interface AdmitAIAgentDefinitionSelectionInput {
|
|
83
|
+
readonly selectionOwner?: AIAgentDefinitionSelectionOwner;
|
|
73
84
|
readonly requirement: AIAgentTaskRequirementProjection;
|
|
74
85
|
readonly candidateSet: AIAgentDefinitionCandidateSet;
|
|
75
86
|
readonly decision: AIAgentDefinitionSelectionDecision;
|
|
87
|
+
/** Required for revise-existing; an authentic immutable proof from the previous execution. */
|
|
88
|
+
readonly previousExecution?: FrozenAIAgentTaskBinding;
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
export interface FreezeSelectedAIAgentTaskBindingInput {
|
|
92
|
+
readonly executionResources?: AIAgentExecutionResourceObservation;
|
|
93
|
+
readonly selectionOwner?: AIAgentDefinitionSelectionOwner;
|
|
94
|
+
readonly codeExecutions?: readonly CompiledAIAgentCodeExecution[];
|
|
79
95
|
readonly registry: EffectiveResourceRegistry;
|
|
80
96
|
readonly projection: AIWorkflowAgentResourceProjection;
|
|
81
97
|
readonly contentIdentities: ReadonlyMap<string, ResourceContentIdentity>;
|
|
@@ -86,6 +102,9 @@ export interface FreezeSelectedAIAgentTaskBindingInput {
|
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
interface CandidateSetAuthority {
|
|
105
|
+
readonly executionResources?: AIAgentExecutionResourceObservation;
|
|
106
|
+
readonly selectionOwner?: AIAgentDefinitionSelectionOwner;
|
|
107
|
+
readonly generation: number;
|
|
89
108
|
readonly registry: EffectiveResourceRegistry;
|
|
90
109
|
readonly projection: AIWorkflowAgentResourceProjection;
|
|
91
110
|
readonly layers: readonly ResourceLayerContentIdentityInput[];
|
|
@@ -96,13 +115,70 @@ interface AuthoringAdmissionAuthority {
|
|
|
96
115
|
readonly candidateSet: AIAgentDefinitionCandidateSet;
|
|
97
116
|
readonly requirement: AIAgentTaskRequirementProjection;
|
|
98
117
|
readonly proposalDigest: Sha256Digest;
|
|
118
|
+
readonly before?: {
|
|
119
|
+
readonly authorityDigest: Sha256Digest;
|
|
120
|
+
readonly effectiveOrigin: ResourceOrigin;
|
|
121
|
+
readonly authorityResourceIds: readonly string[];
|
|
122
|
+
};
|
|
99
123
|
}
|
|
100
124
|
|
|
101
125
|
const authenticRequirements = new WeakSet<object>();
|
|
102
126
|
const candidateSetAuthorities = new WeakMap<object, CandidateSetAuthority>();
|
|
103
127
|
const selectedAdmissionAuthorities = new WeakMap<object, CandidateSetAuthority>();
|
|
104
128
|
const authoringAdmissionAuthorities = new WeakMap<object, AuthoringAdmissionAuthority>();
|
|
105
|
-
const
|
|
129
|
+
declare const selectionOwnerBrand: unique symbol;
|
|
130
|
+
|
|
131
|
+
/** Process-local state dependency. It is not a resource identity or a serializable receipt. */
|
|
132
|
+
export interface AIAgentDefinitionSelectionOwner {
|
|
133
|
+
readonly [selectionOwnerBrand]: true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface SelectionOwnerState {
|
|
137
|
+
readonly pending: Map<`resource://${string}`, object>;
|
|
138
|
+
readonly receiptAuthority: Pick<ResourceAuthoringTransactionPort, 'loadReceipt'>;
|
|
139
|
+
generation: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface CreateAIAgentDefinitionSelectionOwnerInput {
|
|
143
|
+
readonly receiptAuthority: Pick<ResourceAuthoringTransactionPort, 'loadReceipt'>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const selectionOwnerStates = new WeakMap<AIAgentDefinitionSelectionOwner, SelectionOwnerState>();
|
|
147
|
+
// This association prevents removing/changing the explicit owner on the same observation.
|
|
148
|
+
// Pending state lives with the stable owner, never with this per-observation guard.
|
|
149
|
+
const registryOwners = new WeakMap<EffectiveResourceRegistry, {
|
|
150
|
+
readonly owner: AIAgentDefinitionSelectionOwner;
|
|
151
|
+
readonly generation: number;
|
|
152
|
+
}>();
|
|
153
|
+
|
|
154
|
+
export function createAIAgentDefinitionSelectionOwner(
|
|
155
|
+
input: CreateAIAgentDefinitionSelectionOwnerInput,
|
|
156
|
+
): AIAgentDefinitionSelectionOwner {
|
|
157
|
+
const owner = Object.freeze({}) as AIAgentDefinitionSelectionOwner;
|
|
158
|
+
selectionOwnerStates.set(owner, { pending: new Map(), generation: 0, receiptAuthority: input.receiptAuthority });
|
|
159
|
+
return owner;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function ownerMatches(
|
|
163
|
+
authority: CandidateSetAuthority,
|
|
164
|
+
owner: AIAgentDefinitionSelectionOwner | undefined,
|
|
165
|
+
): boolean {
|
|
166
|
+
return authority.selectionOwner === owner
|
|
167
|
+
&& registryOwners.get(authority.registry)?.owner === owner
|
|
168
|
+
&& (owner === undefined || selectionOwnerStates.has(owner));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isSuperseded(authority: CandidateSetAuthority): boolean {
|
|
172
|
+
if (!authority.selectionOwner) return false;
|
|
173
|
+
const generation = selectionOwnerStates.get(authority.selectionOwner)!.generation;
|
|
174
|
+
return authority.generation !== generation
|
|
175
|
+
|| registryOwners.get(authority.registry)?.generation !== generation;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function pendingFor(authority: CandidateSetAuthority): SelectionOwnerState['pending'] | undefined {
|
|
179
|
+
return authority.selectionOwner === undefined
|
|
180
|
+
? undefined : selectionOwnerStates.get(authority.selectionOwner)?.pending;
|
|
181
|
+
}
|
|
106
182
|
|
|
107
183
|
export function normalizeAIAgentTaskRequirement(
|
|
108
184
|
value: AIAgentTaskRequirement,
|
|
@@ -159,6 +235,18 @@ export function projectAIAgentDefinitionCandidates(
|
|
|
159
235
|
input: ProjectAIAgentDefinitionCandidatesInput,
|
|
160
236
|
): AIAgentDefinitionCandidateSet {
|
|
161
237
|
assertProjectionAuthority(input);
|
|
238
|
+
const ownerState = input.selectionOwner === undefined ? undefined : selectionOwnerStates.get(input.selectionOwner);
|
|
239
|
+
const registeredOwner = registryOwners.get(input.registry);
|
|
240
|
+
if ((input.selectionOwner !== undefined && !ownerState)
|
|
241
|
+
|| (registeredOwner && registeredOwner.owner !== input.selectionOwner)) {
|
|
242
|
+
throw new AIAgentDefinitionSelectionError(
|
|
243
|
+
'AI_AGENT_SELECTION_OWNER_MISMATCH',
|
|
244
|
+
'Candidate projection requires the authentic selection owner already bound to this registry observation.',
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (input.selectionOwner && !registeredOwner) {
|
|
248
|
+
registryOwners.set(input.registry, { owner: input.selectionOwner, generation: ownerState!.generation });
|
|
249
|
+
}
|
|
162
250
|
const contentIdentities = resolveEffectiveResourceContentIdentities({
|
|
163
251
|
registry: input.registry,
|
|
164
252
|
layers: input.layers,
|
|
@@ -167,7 +255,7 @@ export function projectAIAgentDefinitionCandidates(
|
|
|
167
255
|
registry: input.registry,
|
|
168
256
|
layers: input.layers,
|
|
169
257
|
}), 'registry revision');
|
|
170
|
-
const candidates = input.projection.agentDefinitions
|
|
258
|
+
const candidates = (input.executionResources?.projection ?? input.projection).agentDefinitions
|
|
171
259
|
.map((agent): AIAgentDefinitionCandidate => {
|
|
172
260
|
const identity = contentIdentities.get(agent.resource.resourceId);
|
|
173
261
|
if (!identity) {
|
|
@@ -210,6 +298,9 @@ export function projectAIAgentDefinitionCandidates(
|
|
|
210
298
|
candidateSetDigest: candidateSetDigest(registryRevision, candidates),
|
|
211
299
|
}) satisfies AIAgentDefinitionCandidateSet;
|
|
212
300
|
candidateSetAuthorities.set(candidateSet, Object.freeze({
|
|
301
|
+
executionResources: input.executionResources,
|
|
302
|
+
selectionOwner: input.selectionOwner,
|
|
303
|
+
generation: registryOwners.get(input.registry)?.generation ?? 0,
|
|
213
304
|
registry: input.registry,
|
|
214
305
|
projection: input.projection,
|
|
215
306
|
layers: input.layers,
|
|
@@ -222,21 +313,42 @@ export async function reconcileAuthoredAIAgentDefinition(
|
|
|
222
313
|
): Promise<AIAgentDefinitionCandidateSet> {
|
|
223
314
|
const authoringAuthority = authoringAdmissionAuthorities.get(input.authoringAdmission as object);
|
|
224
315
|
const authority = candidateSetAuthorities.get(input.candidateSet);
|
|
316
|
+
if (!authority || !input.selectionOwner || !ownerMatches(authority, input.selectionOwner)
|
|
317
|
+
|| authoringAuthority?.candidateSetAuthority.selectionOwner !== input.selectionOwner) {
|
|
318
|
+
throw new AIAgentDefinitionSelectionError(
|
|
319
|
+
'AI_AGENT_SELECTION_OWNER_MISMATCH',
|
|
320
|
+
'Authoring reconciliation requires the same explicit owner as its pending admission and current observation.',
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const ownerState = selectionOwnerStates.get(input.selectionOwner)!;
|
|
324
|
+
if (input.receiptAuthority !== ownerState.receiptAuthority) {
|
|
325
|
+
throw new AIAgentDefinitionSelectionError(
|
|
326
|
+
'AI_AGENT_SELECTION_OWNER_MISMATCH',
|
|
327
|
+
'Authoring reconciliation requires the receipt authority explicitly bound to this selection owner.',
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
if (isSuperseded(authority)) {
|
|
331
|
+
throw new AIAgentDefinitionSelectionError(
|
|
332
|
+
'AI_AGENT_AUTHORING_RECEIPT_STALE',
|
|
333
|
+
'Authoring reconciliation cannot restore a superseded registry observation.',
|
|
334
|
+
);
|
|
335
|
+
}
|
|
225
336
|
if (!authority
|
|
226
337
|
|| authority.registry !== input.registry
|
|
227
338
|
|| authority.projection !== input.projection
|
|
339
|
+
|| authority.executionResources !== input.executionResources
|
|
228
340
|
|| authority.layers !== input.layers
|
|
229
341
|
|| !authoringAuthority
|
|
230
342
|
|| candidateSetAuthorities.get(authoringAuthority.candidateSet)
|
|
231
343
|
!== authoringAuthority.candidateSetAuthority
|
|
232
344
|
|| authoringAuthority.requirement.requirementDigest
|
|
233
345
|
!== input.authoringAdmission.requirementDigest
|
|
234
|
-
||
|
|
346
|
+
|| ownerState.pending.get(input.authoringAdmission.agentDefinitionRef)
|
|
235
347
|
!== input.authoringAdmission
|
|
236
348
|
|| authoringAuthority.proposalDigest !== digest(input.authoringAdmission.proposal)) {
|
|
237
349
|
throw new AIAgentDefinitionSelectionError(
|
|
238
350
|
'AI_AGENT_CANDIDATE_PROJECTION_UNTRUSTED',
|
|
239
|
-
'Authoring reconciliation requires the exact
|
|
351
|
+
'Authoring reconciliation requires the exact pending admission and candidate projection for this registry observation.',
|
|
240
352
|
);
|
|
241
353
|
}
|
|
242
354
|
validateReceiptShapeAndDigest(input.receipt);
|
|
@@ -248,6 +360,13 @@ export async function reconcileAuthoredAIAgentDefinition(
|
|
|
248
360
|
);
|
|
249
361
|
}
|
|
250
362
|
validateReceiptShapeAndDigest(durableReceipt);
|
|
363
|
+
if (isSuperseded(authority)
|
|
364
|
+
|| ownerState.pending.get(input.authoringAdmission.agentDefinitionRef) !== input.authoringAdmission) {
|
|
365
|
+
throw new AIAgentDefinitionSelectionError(
|
|
366
|
+
'AI_AGENT_AUTHORING_RECEIPT_STALE',
|
|
367
|
+
'The pending admission or registry observation was superseded while loading the durable receipt.',
|
|
368
|
+
);
|
|
369
|
+
}
|
|
251
370
|
const proposal = input.authoringAdmission.proposal;
|
|
252
371
|
const currentRevision = asDigest(deriveResourceAuthoringRegistryRevision({
|
|
253
372
|
registry: input.registry,
|
|
@@ -281,14 +400,16 @@ export async function reconcileAuthoredAIAgentDefinition(
|
|
|
281
400
|
|| input.receipt.catalogId !== proposal.catalogId
|
|
282
401
|
|| input.receipt.resourceId !== proposal.resourceId
|
|
283
402
|
|| input.receipt.kind !== proposal.kind
|
|
284
|
-
|| input.receipt.
|
|
403
|
+
|| input.receipt.envelopeVersion !== proposal.envelopeVersion
|
|
404
|
+
|| input.receipt.writerSpecVersion !== proposal.writerSpecVersion
|
|
285
405
|
|| input.receipt.sourceShape !== proposal.sourceShape
|
|
286
406
|
|| input.receipt.documentUri !== proposal.documentUri
|
|
287
|
-
|| input.receipt.authorityDigestBefore !== null
|
|
407
|
+
|| input.receipt.authorityDigestBefore !== (authoringAuthority.before?.authorityDigest ?? null)
|
|
288
408
|
|| input.receipt.authorityDigestAfter !== sha256Digest(proposal.authorityText)
|
|
289
409
|
|| input.receipt.registryRevisionBefore !== proposal.expected.registryRevision
|
|
290
410
|
|| input.receipt.kind !== 'AIAgentDefinition'
|
|
291
|
-
|| resource.metadata.
|
|
411
|
+
|| resource.metadata.envelopeVersion !== input.receipt.envelopeVersion
|
|
412
|
+
|| resource.metadata.specVersion !== input.receipt.writerSpecVersion
|
|
292
413
|
|| resource.sourceShape !== input.receipt.sourceShape
|
|
293
414
|
|| resource.documentUri !== input.receipt.documentUri
|
|
294
415
|
|| !identity
|
|
@@ -296,6 +417,8 @@ export async function reconcileAuthoredAIAgentDefinition(
|
|
|
296
417
|
|| identity.contentDigest !== input.receipt.contentDigest
|
|
297
418
|
|| !effectiveOrigin
|
|
298
419
|
|| !sameReceiptOrigin(effectiveOrigin, input.receipt.effectiveOrigin)
|
|
420
|
+
|| (authoringAuthority.before !== undefined
|
|
421
|
+
&& canonicalJson(authorityResourceIds) !== canonicalJson(authoringAuthority.before.authorityResourceIds))
|
|
299
422
|
|| canonicalJson(authorityResourceIds) !== canonicalJson([...input.receipt.authorityResourceIds].sort(compareCodeUnits))
|
|
300
423
|
|| !candidate
|
|
301
424
|
|| candidate.contentDigest !== input.receipt.contentDigest) {
|
|
@@ -309,6 +432,15 @@ export async function reconcileAuthoredAIAgentDefinition(
|
|
|
309
432
|
planDigest: asDigest(input.receipt.planDigest, 'receipt plan digest'),
|
|
310
433
|
transactionId: input.receipt.transactionId,
|
|
311
434
|
receiptDigest: asDigest(input.receipt.receiptDigest, 'receipt digest'),
|
|
435
|
+
...(input.authoringAdmission.status === 'revision-request' ? {
|
|
436
|
+
revision: Object.freeze({
|
|
437
|
+
candidateRef: input.authoringAdmission.candidate.agentDefinitionRef,
|
|
438
|
+
candidateDigest: input.authoringAdmission.candidate.candidateDigest,
|
|
439
|
+
contentDigest: input.authoringAdmission.candidate.contentDigest,
|
|
440
|
+
registryRevision: input.authoringAdmission.candidate.registryRevision,
|
|
441
|
+
feedback: input.authoringAdmission.feedback,
|
|
442
|
+
}),
|
|
443
|
+
} : {}),
|
|
312
444
|
}) satisfies AIAgentDefinitionAuthoringEvidence;
|
|
313
445
|
const candidates = Object.freeze(input.candidateSet.candidates.map((value) => {
|
|
314
446
|
if (value !== candidate) return value;
|
|
@@ -327,8 +459,10 @@ export async function reconcileAuthoredAIAgentDefinition(
|
|
|
327
459
|
candidates,
|
|
328
460
|
candidateSetDigest: candidateSetDigest(input.candidateSet.registryRevision, candidates),
|
|
329
461
|
}) satisfies AIAgentDefinitionCandidateSet;
|
|
330
|
-
|
|
331
|
-
|
|
462
|
+
if (authoringAuthority.before) ownerState.generation += 1;
|
|
463
|
+
registryOwners.set(input.registry, { owner: input.selectionOwner, generation: ownerState.generation });
|
|
464
|
+
candidateSetAuthorities.set(reconciled, Object.freeze({ ...authority, generation: ownerState.generation }));
|
|
465
|
+
ownerState.pending.delete(input.authoringAdmission.agentDefinitionRef);
|
|
332
466
|
return reconciled;
|
|
333
467
|
}
|
|
334
468
|
|
|
@@ -343,7 +477,11 @@ export function admitAIAgentDefinitionSelection(
|
|
|
343
477
|
return rejection('requirement-stale', 'The decision is not bound to the current normalized task requirement.');
|
|
344
478
|
}
|
|
345
479
|
const candidateSetAuthority = candidateSetAuthorities.get(input.candidateSet);
|
|
480
|
+
if (candidateSetAuthority && !ownerMatches(candidateSetAuthority, input.selectionOwner)) {
|
|
481
|
+
return rejection('selection-owner-mismatch', 'Selection requires the same explicit owner as its candidate observation.');
|
|
482
|
+
}
|
|
346
483
|
if (!candidateSetAuthority
|
|
484
|
+
|| isSuperseded(candidateSetAuthority)
|
|
347
485
|
|| input.decision.candidateSetDigest !== input.candidateSet.candidateSetDigest) {
|
|
348
486
|
return rejection('candidate-set-stale', 'The decision is not bound to the current authentic candidate set.');
|
|
349
487
|
}
|
|
@@ -352,11 +490,10 @@ export function admitAIAgentDefinitionSelection(
|
|
|
352
490
|
const candidate = input.candidateSet.candidates.find(({ agentDefinitionRef }) =>
|
|
353
491
|
agentDefinitionRef === decision.candidateRef);
|
|
354
492
|
if (!candidate) return rejection('candidate-missing', 'The selected candidate is absent from the current set.');
|
|
355
|
-
if (
|
|
356
|
-
&& candidate.authoringEvidence === undefined) {
|
|
493
|
+
if (pendingFor(candidateSetAuthority)?.has(candidate.agentDefinitionRef)) {
|
|
357
494
|
return rejection(
|
|
358
495
|
'authoring-in-progress',
|
|
359
|
-
'An in-flight
|
|
496
|
+
'An in-flight authoring transition must reconcile its durable Halfcode receipt before this definition is selectable.',
|
|
360
497
|
);
|
|
361
498
|
}
|
|
362
499
|
if (candidate.candidateDigest !== decision.candidateDigest) {
|
|
@@ -376,6 +513,47 @@ export function admitAIAgentDefinitionSelection(
|
|
|
376
513
|
selectedAdmissionAuthorities.set(admission, candidateSetAuthority);
|
|
377
514
|
return admission;
|
|
378
515
|
}
|
|
516
|
+
if (!input.selectionOwner) {
|
|
517
|
+
return rejection('selection-owner-required', 'Authoring requires an explicit stable selection owner shared across registry refreshes.');
|
|
518
|
+
}
|
|
519
|
+
const pending = pendingFor(candidateSetAuthority)!;
|
|
520
|
+
if (input.decision.mode === 'revise-existing') {
|
|
521
|
+
const decision = input.decision;
|
|
522
|
+
const candidate = input.candidateSet.candidates.find(({ agentDefinitionRef }) => agentDefinitionRef === decision.candidateRef);
|
|
523
|
+
if (!candidate) return rejection('candidate-missing', 'The revision candidate is absent from the current set.');
|
|
524
|
+
if (candidate.candidateDigest !== decision.candidateDigest) {
|
|
525
|
+
return rejection('candidate-digest-mismatch', 'The revision candidate digest is stale or forged.');
|
|
526
|
+
}
|
|
527
|
+
if (!matchesRevisionFeedback(input, decision, candidate)) {
|
|
528
|
+
return rejection('revision-feedback-invalid', 'Revision feedback must match the exact candidate, requirement, observation and authentic prior execution.');
|
|
529
|
+
}
|
|
530
|
+
const before = revisionAuthority(candidateSetAuthority, candidate);
|
|
531
|
+
if (!before || !isValidAuthoringProposal(decision.proposal, decision.candidateRef, input.candidateSet, before.authorityDigest)
|
|
532
|
+
|| decision.proposal.documentUri !== before.effectiveOrigin.documentUri) {
|
|
533
|
+
return rejection('authoring-proposal-invalid', 'Revision requires an exact native update/present proposal against the effective authority digest and document.');
|
|
534
|
+
}
|
|
535
|
+
if (pending.has(candidate.agentDefinitionRef)) {
|
|
536
|
+
return rejection('authoring-in-progress', 'An authoring request for this exact Agent definition is already in flight.');
|
|
537
|
+
}
|
|
538
|
+
const proposal = cloneAuthoringProposal(decision.proposal);
|
|
539
|
+
const admission = Object.freeze({
|
|
540
|
+
schemaVersion: AI_AGENT_DEFINITION_SELECTION_SCHEMA_VERSION,
|
|
541
|
+
status: 'revision-request' as const,
|
|
542
|
+
requirementDigest: input.requirement.requirementDigest,
|
|
543
|
+
candidateSetDigest: input.candidateSet.candidateSetDigest,
|
|
544
|
+
agentDefinitionRef: candidate.agentDefinitionRef,
|
|
545
|
+
candidate,
|
|
546
|
+
feedback: Object.freeze({ ...decision.feedback }),
|
|
547
|
+
proposal,
|
|
548
|
+
reason: decision.reason,
|
|
549
|
+
}) satisfies ReviseExistingAIAgentDefinitionAdmission;
|
|
550
|
+
authoringAdmissionAuthorities.set(admission, Object.freeze({
|
|
551
|
+
candidateSetAuthority, candidateSet: input.candidateSet, requirement: input.requirement,
|
|
552
|
+
proposalDigest: digest(proposal), before,
|
|
553
|
+
}));
|
|
554
|
+
pending.set(candidate.agentDefinitionRef, admission);
|
|
555
|
+
return admission;
|
|
556
|
+
}
|
|
379
557
|
if (!isValidAuthoringProposal(input.decision.proposal, input.decision.agentDefinitionRef, input.candidateSet)) {
|
|
380
558
|
return rejection('authoring-proposal-invalid', 'The author-new decision is not one exact Halfcode create proposal for the current registry.');
|
|
381
559
|
}
|
|
@@ -389,7 +567,7 @@ export function admitAIAgentDefinitionSelection(
|
|
|
389
567
|
proposal,
|
|
390
568
|
reason: stringValue(input.decision.reason, 'decision.reason'),
|
|
391
569
|
}) satisfies AuthorNewAIAgentDefinitionAdmission;
|
|
392
|
-
if (
|
|
570
|
+
if (pending.has(admission.agentDefinitionRef)) {
|
|
393
571
|
return rejection('authoring-in-progress', 'An authoring request for this exact Agent definition is already in flight.');
|
|
394
572
|
}
|
|
395
573
|
authoringAdmissionAuthorities.set(admission, Object.freeze({
|
|
@@ -398,7 +576,7 @@ export function admitAIAgentDefinitionSelection(
|
|
|
398
576
|
requirement: input.requirement,
|
|
399
577
|
proposalDigest: digest(proposal),
|
|
400
578
|
}));
|
|
401
|
-
|
|
579
|
+
pending.set(admission.agentDefinitionRef, admission);
|
|
402
580
|
return admission;
|
|
403
581
|
}
|
|
404
582
|
|
|
@@ -407,8 +585,12 @@ export function freezeSelectedAIAgentTaskBinding(
|
|
|
407
585
|
): FrozenAIAgentTaskBinding {
|
|
408
586
|
const authority = selectedAdmissionAuthorities.get(input.admission as object);
|
|
409
587
|
if (!authority
|
|
588
|
+
|| !ownerMatches(authority, input.selectionOwner)
|
|
589
|
+
|| isSuperseded(authority)
|
|
590
|
+
|| pendingFor(authority)?.has(input.admission.candidate.agentDefinitionRef)
|
|
410
591
|
|| authority.registry !== input.registry
|
|
411
592
|
|| authority.projection !== input.projection
|
|
593
|
+
|| authority.executionResources !== input.executionResources
|
|
412
594
|
|| input.contentIdentities.get(resourceId(input.admission.candidate.agentDefinitionRef))?.contentDigest
|
|
413
595
|
!== input.admission.candidate.contentDigest) {
|
|
414
596
|
throw new AIAgentDefinitionSelectionError(
|
|
@@ -417,6 +599,8 @@ export function freezeSelectedAIAgentTaskBinding(
|
|
|
417
599
|
);
|
|
418
600
|
}
|
|
419
601
|
return projectFrozenAIAgentTaskBinding(freezeAIWorkflowRunResources({
|
|
602
|
+
executionResources: input.executionResources,
|
|
603
|
+
codeExecutions: input.codeExecutions,
|
|
420
604
|
registry: input.registry,
|
|
421
605
|
projection: input.projection,
|
|
422
606
|
task: {
|
|
@@ -462,6 +646,20 @@ function isValidDecision(value: AIAgentDefinitionSelectionDecision): boolean {
|
|
|
462
646
|
exactRef(decision.agentDefinitionRef, 'decision.agentDefinitionRef');
|
|
463
647
|
return true;
|
|
464
648
|
}
|
|
649
|
+
if (decision.mode === 'revise-existing') {
|
|
650
|
+
exactKeys(decision, ['schemaVersion', 'mode', 'requirementDigest', 'candidateSetDigest',
|
|
651
|
+
'candidateRef', 'candidateDigest', 'feedback', 'proposal', 'reason'], 'decision');
|
|
652
|
+
exactRef(decision.candidateRef, 'decision.candidateRef');
|
|
653
|
+
const feedback = closedRecord(decision.feedback, 'decision.feedback');
|
|
654
|
+
exactKeys(feedback, ['observationRef', 'attemptRef', 'verificationRef', 'requirementDigest',
|
|
655
|
+
'candidateSetDigest', 'candidateDigest', 'previousExecutionFingerprint'], 'decision.feedback');
|
|
656
|
+
for (const key of ['observationRef', 'attemptRef', 'verificationRef'] as const) {
|
|
657
|
+
nonEmptyString(feedback[key], `decision.feedback.${key}`);
|
|
658
|
+
}
|
|
659
|
+
return isDigest(decision.candidateDigest)
|
|
660
|
+
&& ['requirementDigest', 'candidateSetDigest', 'candidateDigest', 'previousExecutionFingerprint']
|
|
661
|
+
.every((key) => isDigest(feedback[key]));
|
|
662
|
+
}
|
|
465
663
|
return false;
|
|
466
664
|
} catch {
|
|
467
665
|
return false;
|
|
@@ -469,6 +667,7 @@ function isValidDecision(value: AIAgentDefinitionSelectionDecision): boolean {
|
|
|
469
667
|
}
|
|
470
668
|
|
|
471
669
|
function assertProjectionAuthority(input: ProjectAIAgentDefinitionCandidatesInput): void {
|
|
670
|
+
if (input.executionResources) assertAIAgentExecutionResources(input.executionResources, input.registry, input.layers);
|
|
472
671
|
if (sourceRegistryForAIWorkflowAgentResourceProjection(input.projection) !== input.registry) {
|
|
473
672
|
throw new AIAgentDefinitionSelectionError(
|
|
474
673
|
'AI_AGENT_CANDIDATE_PROJECTION_UNTRUSTED',
|
|
@@ -491,10 +690,49 @@ function isCompatible(
|
|
|
491
690
|
&& containsAll(candidate.messageSourceRefs, requirement.requiredMessageSourceRefs);
|
|
492
691
|
}
|
|
493
692
|
|
|
693
|
+
function matchesRevisionFeedback(
|
|
694
|
+
input: AdmitAIAgentDefinitionSelectionInput,
|
|
695
|
+
decision: ReviseExistingAIAgentDefinitionDecision,
|
|
696
|
+
candidate: AIAgentDefinitionCandidate,
|
|
697
|
+
): boolean {
|
|
698
|
+
try {
|
|
699
|
+
const execution = input.previousExecution;
|
|
700
|
+
return execution !== undefined
|
|
701
|
+
&& frozenAIAgentTaskAgentContentDigest(execution) === candidate.contentDigest
|
|
702
|
+
&& execution.task.agentDefinitionRef === candidate.agentDefinitionRef
|
|
703
|
+
&& execution.semanticFingerprint === decision.feedback.previousExecutionFingerprint
|
|
704
|
+
&& decision.feedback.requirementDigest === input.requirement.requirementDigest
|
|
705
|
+
&& decision.feedback.candidateSetDigest === input.candidateSet.candidateSetDigest
|
|
706
|
+
&& decision.feedback.candidateDigest === candidate.candidateDigest;
|
|
707
|
+
} catch {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function revisionAuthority(
|
|
713
|
+
authority: CandidateSetAuthority,
|
|
714
|
+
candidate: AIAgentDefinitionCandidate,
|
|
715
|
+
): AuthoringAdmissionAuthority['before'] {
|
|
716
|
+
const id = resourceId(candidate.agentDefinitionRef);
|
|
717
|
+
const entry = authority.registry.byId.get(id);
|
|
718
|
+
const identity = resolveEffectiveResourceContentIdentities(authority).get(id);
|
|
719
|
+
if (!entry?.resource || entry.resource.kind !== 'AIAgentDefinition'
|
|
720
|
+
|| entry.resource.sourceShape !== 'single-file' || !entry.effectiveOrigin
|
|
721
|
+
|| !identity || identity.contentDigest !== candidate.contentDigest) return undefined;
|
|
722
|
+
return Object.freeze({
|
|
723
|
+
authorityDigest: asDigest(identity.authorityDigest, 'previous authority digest'),
|
|
724
|
+
effectiveOrigin: Object.freeze({ ...entry.effectiveOrigin }),
|
|
725
|
+
authorityResourceIds: Object.freeze([...authority.registry.byId.values()]
|
|
726
|
+
.filter((value) => value.resource && sameOrigin(value.effectiveOrigin, entry.effectiveOrigin))
|
|
727
|
+
.map(({ resourceId }) => resourceId).sort(compareCodeUnits)),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
494
731
|
function isValidAuthoringProposal(
|
|
495
732
|
value: ResourceAuthoringProposal,
|
|
496
733
|
definitionRef: `resource://${string}`,
|
|
497
734
|
candidateSet: AIAgentDefinitionCandidateSet,
|
|
735
|
+
authorityDigestBefore?: Sha256Digest,
|
|
498
736
|
): boolean {
|
|
499
737
|
try {
|
|
500
738
|
const proposal = closedRecord(value, 'proposal');
|
|
@@ -504,24 +742,31 @@ function isValidAuthoringProposal(
|
|
|
504
742
|
'catalogId',
|
|
505
743
|
'resourceId',
|
|
506
744
|
'kind',
|
|
507
|
-
'
|
|
745
|
+
'envelopeVersion',
|
|
746
|
+
'writerSpecVersion',
|
|
508
747
|
'sourceShape',
|
|
509
748
|
'documentUri',
|
|
510
749
|
'authorityText',
|
|
511
750
|
'expected',
|
|
512
751
|
], 'proposal');
|
|
513
752
|
const expected = closedRecord(proposal.expected, 'proposal.expected');
|
|
514
|
-
exactKeys(expected,
|
|
753
|
+
exactKeys(expected, authorityDigestBefore === undefined
|
|
754
|
+
? ['state', 'registryRevision'] : ['state', 'registryRevision', 'authorityDigest'], 'proposal.expected');
|
|
515
755
|
return proposal.schemaVersion === RESOURCE_AUTHORING_SCHEMA_VERSION
|
|
516
|
-
&& proposal.operation === 'create'
|
|
756
|
+
&& proposal.operation === (authorityDigestBefore === undefined ? 'create' : 'update')
|
|
517
757
|
&& proposal.kind === 'AIAgentDefinition'
|
|
518
758
|
&& proposal.sourceShape === 'single-file'
|
|
519
|
-
&& expected.state === 'absent'
|
|
759
|
+
&& expected.state === (authorityDigestBefore === undefined ? 'absent' : 'present')
|
|
760
|
+
&& (authorityDigestBefore === undefined || (expected.authorityDigest === authorityDigestBefore
|
|
761
|
+
&& sha256Digest(stringValue(proposal.authorityText, 'proposal.authorityText')) !== authorityDigestBefore))
|
|
520
762
|
&& expected.registryRevision === candidateSet.registryRevision
|
|
521
763
|
&& definitionRef === resourceRef(nonEmptyString(proposal.resourceId, 'proposal.resourceId'))
|
|
522
|
-
&&
|
|
764
|
+
&& (authorityDigestBefore !== undefined
|
|
765
|
+
|| !candidateSet.candidates.some(({ agentDefinitionRef }) => agentDefinitionRef === definitionRef))
|
|
523
766
|
&& nonEmptyString(proposal.catalogId, 'proposal.catalogId').length > 0
|
|
524
|
-
&&
|
|
767
|
+
&& proposal.envelopeVersion === RESOURCE_ENVELOPE_VERSION
|
|
768
|
+
&& Number.isSafeInteger(proposal.writerSpecVersion)
|
|
769
|
+
&& Number(proposal.writerSpecVersion) > 0
|
|
525
770
|
&& /^vfs:\/\/@\/.+\.xnl$/u.test(nonEmptyString(proposal.documentUri, 'proposal.documentUri'))
|
|
526
771
|
&& stringValue(proposal.authorityText, 'proposal.authorityText').length > 0;
|
|
527
772
|
} catch {
|
|
@@ -536,7 +781,8 @@ function cloneAuthoringProposal(value: ResourceAuthoringProposal): ResourceAutho
|
|
|
536
781
|
catalogId: value.catalogId,
|
|
537
782
|
resourceId: value.resourceId,
|
|
538
783
|
kind: value.kind,
|
|
539
|
-
|
|
784
|
+
envelopeVersion: value.envelopeVersion,
|
|
785
|
+
writerSpecVersion: value.writerSpecVersion,
|
|
540
786
|
sourceShape: value.sourceShape,
|
|
541
787
|
documentUri: value.documentUri,
|
|
542
788
|
authorityText: value.authorityText,
|
|
@@ -555,7 +801,8 @@ function validateReceiptShapeAndDigest(receiptValue: ResourceAuthoringReceipt):
|
|
|
555
801
|
'catalogId',
|
|
556
802
|
'resourceId',
|
|
557
803
|
'kind',
|
|
558
|
-
'
|
|
804
|
+
'envelopeVersion',
|
|
805
|
+
'writerSpecVersion',
|
|
559
806
|
'sourceShape',
|
|
560
807
|
'documentUri',
|
|
561
808
|
'authorityDigestBefore',
|
|
@@ -572,6 +819,9 @@ function validateReceiptShapeAndDigest(receiptValue: ResourceAuthoringReceipt):
|
|
|
572
819
|
if (receipt.schemaVersion !== RESOURCE_AUTHORING_SCHEMA_VERSION
|
|
573
820
|
|| (receipt.operation !== 'create' && receipt.operation !== 'update')
|
|
574
821
|
|| receipt.sourceShape !== 'single-file'
|
|
822
|
+
|| receipt.envelopeVersion !== RESOURCE_ENVELOPE_VERSION
|
|
823
|
+
|| !Number.isSafeInteger(receipt.writerSpecVersion)
|
|
824
|
+
|| Number(receipt.writerSpecVersion) <= 0
|
|
575
825
|
|| (receipt.authorityDigestBefore !== null && !isDigest(receipt.authorityDigestBefore))
|
|
576
826
|
|| !isDigest(receipt.planDigest)
|
|
577
827
|
|| !isDigest(receipt.authorityDigestAfter)
|
|
@@ -587,7 +837,7 @@ function validateReceiptShapeAndDigest(receiptValue: ResourceAuthoringReceipt):
|
|
|
587
837
|
|| (origin.logicalPath !== undefined && typeof origin.logicalPath !== 'string')) {
|
|
588
838
|
throw new Error('invalid receipt fields');
|
|
589
839
|
}
|
|
590
|
-
for (const field of ['transactionId', 'catalogId', 'resourceId', 'kind', '
|
|
840
|
+
for (const field of ['transactionId', 'catalogId', 'resourceId', 'kind', 'envelopeVersion', 'documentUri'] as const) {
|
|
591
841
|
nonEmptyString(receipt[field], `receipt.${field}`);
|
|
592
842
|
}
|
|
593
843
|
for (const field of ['layerId', 'packageId', 'documentUri'] as const) {
|