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.
@@ -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 pendingAuthoringAdmissions = new Map<`resource://${string}`, object>();
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
- || pendingAuthoringAdmissions.get(input.authoringAdmission.agentDefinitionRef)
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 author-new admission and candidate projection for this registry observation.',
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.apiVersion !== proposal.apiVersion
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.apiVersion !== input.receipt.apiVersion
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
- candidateSetAuthorities.set(reconciled, authority);
331
- pendingAuthoringAdmissions.delete(input.authoringAdmission.agentDefinitionRef);
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 (pendingAuthoringAdmissions.has(candidate.agentDefinitionRef)
356
- && candidate.authoringEvidence === undefined) {
493
+ if (pendingFor(candidateSetAuthority)?.has(candidate.agentDefinitionRef)) {
357
494
  return rejection(
358
495
  'authoring-in-progress',
359
- 'An in-flight author-new transition must reconcile its durable Halfcode receipt before this definition is selectable.',
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 (pendingAuthoringAdmissions.has(admission.agentDefinitionRef)) {
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
- pendingAuthoringAdmissions.set(admission.agentDefinitionRef, admission);
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
- 'apiVersion',
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, ['state', 'registryRevision'], 'proposal.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
- && !candidateSet.candidates.some(({ agentDefinitionRef }) => agentDefinitionRef === definitionRef)
764
+ && (authorityDigestBefore !== undefined
765
+ || !candidateSet.candidates.some(({ agentDefinitionRef }) => agentDefinitionRef === definitionRef))
523
766
  && nonEmptyString(proposal.catalogId, 'proposal.catalogId').length > 0
524
- && nonEmptyString(proposal.apiVersion, 'proposal.apiVersion').length > 0
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
- apiVersion: value.apiVersion,
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
- 'apiVersion',
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', 'apiVersion', 'documentUri'] as const) {
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) {