ai-workflow-logic 0.2.0 → 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 {
@@ -32,8 +35,10 @@ import {
32
35
  type Sha256Digest,
33
36
  } from 'halfcode-compiler.xnl/resource-core';
34
37
  import { sourceRegistryForAIWorkflowAgentResourceProjection } from './agent-resource-brand.js';
38
+ import { assertAIAgentExecutionResources, type AIAgentExecutionResourceObservation } from './agent-execution-resources.js';
35
39
  import {
36
40
  freezeAIWorkflowRunResources,
41
+ frozenAIAgentTaskAgentContentDigest,
37
42
  projectFrozenAIAgentTaskBinding,
38
43
  } from './run-freeze.js';
39
44
 
@@ -41,6 +46,7 @@ export type AIAgentDefinitionSelectionErrorCode =
41
46
  | 'AI_AGENT_REQUIREMENT_INVALID'
42
47
  | 'AI_AGENT_CANDIDATE_PROJECTION_UNTRUSTED'
43
48
  | 'AI_AGENT_CANDIDATE_CONTENT_IDENTITY_MISSING'
49
+ | 'AI_AGENT_SELECTION_OWNER_MISMATCH'
44
50
  | 'AI_AGENT_AUTHORING_RECEIPT_INVALID'
45
51
  | 'AI_AGENT_AUTHORING_RECEIPT_STALE'
46
52
  | 'AI_AGENT_AUTHORING_RECEIPT_RESOURCE_MISMATCH'
@@ -57,6 +63,9 @@ export class AIAgentDefinitionSelectionError extends Error {
57
63
  }
58
64
 
59
65
  export interface ProjectAIAgentDefinitionCandidatesInput {
66
+ readonly executionResources?: AIAgentExecutionResourceObservation;
67
+ /** Reuse one owner for all observations of the same resource host. */
68
+ readonly selectionOwner?: AIAgentDefinitionSelectionOwner;
60
69
  readonly registry: EffectiveResourceRegistry;
61
70
  readonly projection: AIWorkflowAgentResourceProjection;
62
71
  readonly layers: readonly ResourceLayerContentIdentityInput[];
@@ -65,18 +74,24 @@ export interface ProjectAIAgentDefinitionCandidatesInput {
65
74
  export interface ReconcileAuthoredAIAgentDefinitionInput
66
75
  extends ProjectAIAgentDefinitionCandidatesInput {
67
76
  readonly candidateSet: AIAgentDefinitionCandidateSet;
68
- readonly authoringAdmission: AuthorNewAIAgentDefinitionAdmission;
77
+ readonly authoringAdmission: AuthorNewAIAgentDefinitionAdmission | ReviseExistingAIAgentDefinitionAdmission;
69
78
  readonly receipt: ResourceAuthoringReceipt;
70
79
  readonly receiptAuthority: Pick<ResourceAuthoringTransactionPort, 'loadReceipt'>;
71
80
  }
72
81
 
73
82
  export interface AdmitAIAgentDefinitionSelectionInput {
83
+ readonly selectionOwner?: AIAgentDefinitionSelectionOwner;
74
84
  readonly requirement: AIAgentTaskRequirementProjection;
75
85
  readonly candidateSet: AIAgentDefinitionCandidateSet;
76
86
  readonly decision: AIAgentDefinitionSelectionDecision;
87
+ /** Required for revise-existing; an authentic immutable proof from the previous execution. */
88
+ readonly previousExecution?: FrozenAIAgentTaskBinding;
77
89
  }
78
90
 
79
91
  export interface FreezeSelectedAIAgentTaskBindingInput {
92
+ readonly executionResources?: AIAgentExecutionResourceObservation;
93
+ readonly selectionOwner?: AIAgentDefinitionSelectionOwner;
94
+ readonly codeExecutions?: readonly CompiledAIAgentCodeExecution[];
80
95
  readonly registry: EffectiveResourceRegistry;
81
96
  readonly projection: AIWorkflowAgentResourceProjection;
82
97
  readonly contentIdentities: ReadonlyMap<string, ResourceContentIdentity>;
@@ -87,6 +102,9 @@ export interface FreezeSelectedAIAgentTaskBindingInput {
87
102
  }
88
103
 
89
104
  interface CandidateSetAuthority {
105
+ readonly executionResources?: AIAgentExecutionResourceObservation;
106
+ readonly selectionOwner?: AIAgentDefinitionSelectionOwner;
107
+ readonly generation: number;
90
108
  readonly registry: EffectiveResourceRegistry;
91
109
  readonly projection: AIWorkflowAgentResourceProjection;
92
110
  readonly layers: readonly ResourceLayerContentIdentityInput[];
@@ -97,13 +115,70 @@ interface AuthoringAdmissionAuthority {
97
115
  readonly candidateSet: AIAgentDefinitionCandidateSet;
98
116
  readonly requirement: AIAgentTaskRequirementProjection;
99
117
  readonly proposalDigest: Sha256Digest;
118
+ readonly before?: {
119
+ readonly authorityDigest: Sha256Digest;
120
+ readonly effectiveOrigin: ResourceOrigin;
121
+ readonly authorityResourceIds: readonly string[];
122
+ };
100
123
  }
101
124
 
102
125
  const authenticRequirements = new WeakSet<object>();
103
126
  const candidateSetAuthorities = new WeakMap<object, CandidateSetAuthority>();
104
127
  const selectedAdmissionAuthorities = new WeakMap<object, CandidateSetAuthority>();
105
128
  const authoringAdmissionAuthorities = new WeakMap<object, AuthoringAdmissionAuthority>();
106
- 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
+ }
107
182
 
108
183
  export function normalizeAIAgentTaskRequirement(
109
184
  value: AIAgentTaskRequirement,
@@ -160,6 +235,18 @@ export function projectAIAgentDefinitionCandidates(
160
235
  input: ProjectAIAgentDefinitionCandidatesInput,
161
236
  ): AIAgentDefinitionCandidateSet {
162
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
+ }
163
250
  const contentIdentities = resolveEffectiveResourceContentIdentities({
164
251
  registry: input.registry,
165
252
  layers: input.layers,
@@ -168,7 +255,7 @@ export function projectAIAgentDefinitionCandidates(
168
255
  registry: input.registry,
169
256
  layers: input.layers,
170
257
  }), 'registry revision');
171
- const candidates = input.projection.agentDefinitions
258
+ const candidates = (input.executionResources?.projection ?? input.projection).agentDefinitions
172
259
  .map((agent): AIAgentDefinitionCandidate => {
173
260
  const identity = contentIdentities.get(agent.resource.resourceId);
174
261
  if (!identity) {
@@ -211,6 +298,9 @@ export function projectAIAgentDefinitionCandidates(
211
298
  candidateSetDigest: candidateSetDigest(registryRevision, candidates),
212
299
  }) satisfies AIAgentDefinitionCandidateSet;
213
300
  candidateSetAuthorities.set(candidateSet, Object.freeze({
301
+ executionResources: input.executionResources,
302
+ selectionOwner: input.selectionOwner,
303
+ generation: registryOwners.get(input.registry)?.generation ?? 0,
214
304
  registry: input.registry,
215
305
  projection: input.projection,
216
306
  layers: input.layers,
@@ -223,21 +313,42 @@ export async function reconcileAuthoredAIAgentDefinition(
223
313
  ): Promise<AIAgentDefinitionCandidateSet> {
224
314
  const authoringAuthority = authoringAdmissionAuthorities.get(input.authoringAdmission as object);
225
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
+ }
226
336
  if (!authority
227
337
  || authority.registry !== input.registry
228
338
  || authority.projection !== input.projection
339
+ || authority.executionResources !== input.executionResources
229
340
  || authority.layers !== input.layers
230
341
  || !authoringAuthority
231
342
  || candidateSetAuthorities.get(authoringAuthority.candidateSet)
232
343
  !== authoringAuthority.candidateSetAuthority
233
344
  || authoringAuthority.requirement.requirementDigest
234
345
  !== input.authoringAdmission.requirementDigest
235
- || pendingAuthoringAdmissions.get(input.authoringAdmission.agentDefinitionRef)
346
+ || ownerState.pending.get(input.authoringAdmission.agentDefinitionRef)
236
347
  !== input.authoringAdmission
237
348
  || authoringAuthority.proposalDigest !== digest(input.authoringAdmission.proposal)) {
238
349
  throw new AIAgentDefinitionSelectionError(
239
350
  'AI_AGENT_CANDIDATE_PROJECTION_UNTRUSTED',
240
- '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.',
241
352
  );
242
353
  }
243
354
  validateReceiptShapeAndDigest(input.receipt);
@@ -249,6 +360,13 @@ export async function reconcileAuthoredAIAgentDefinition(
249
360
  );
250
361
  }
251
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
+ }
252
370
  const proposal = input.authoringAdmission.proposal;
253
371
  const currentRevision = asDigest(deriveResourceAuthoringRegistryRevision({
254
372
  registry: input.registry,
@@ -286,7 +404,7 @@ export async function reconcileAuthoredAIAgentDefinition(
286
404
  || input.receipt.writerSpecVersion !== proposal.writerSpecVersion
287
405
  || input.receipt.sourceShape !== proposal.sourceShape
288
406
  || input.receipt.documentUri !== proposal.documentUri
289
- || input.receipt.authorityDigestBefore !== null
407
+ || input.receipt.authorityDigestBefore !== (authoringAuthority.before?.authorityDigest ?? null)
290
408
  || input.receipt.authorityDigestAfter !== sha256Digest(proposal.authorityText)
291
409
  || input.receipt.registryRevisionBefore !== proposal.expected.registryRevision
292
410
  || input.receipt.kind !== 'AIAgentDefinition'
@@ -299,6 +417,8 @@ export async function reconcileAuthoredAIAgentDefinition(
299
417
  || identity.contentDigest !== input.receipt.contentDigest
300
418
  || !effectiveOrigin
301
419
  || !sameReceiptOrigin(effectiveOrigin, input.receipt.effectiveOrigin)
420
+ || (authoringAuthority.before !== undefined
421
+ && canonicalJson(authorityResourceIds) !== canonicalJson(authoringAuthority.before.authorityResourceIds))
302
422
  || canonicalJson(authorityResourceIds) !== canonicalJson([...input.receipt.authorityResourceIds].sort(compareCodeUnits))
303
423
  || !candidate
304
424
  || candidate.contentDigest !== input.receipt.contentDigest) {
@@ -312,6 +432,15 @@ export async function reconcileAuthoredAIAgentDefinition(
312
432
  planDigest: asDigest(input.receipt.planDigest, 'receipt plan digest'),
313
433
  transactionId: input.receipt.transactionId,
314
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
+ } : {}),
315
444
  }) satisfies AIAgentDefinitionAuthoringEvidence;
316
445
  const candidates = Object.freeze(input.candidateSet.candidates.map((value) => {
317
446
  if (value !== candidate) return value;
@@ -330,8 +459,10 @@ export async function reconcileAuthoredAIAgentDefinition(
330
459
  candidates,
331
460
  candidateSetDigest: candidateSetDigest(input.candidateSet.registryRevision, candidates),
332
461
  }) satisfies AIAgentDefinitionCandidateSet;
333
- candidateSetAuthorities.set(reconciled, authority);
334
- 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);
335
466
  return reconciled;
336
467
  }
337
468
 
@@ -346,7 +477,11 @@ export function admitAIAgentDefinitionSelection(
346
477
  return rejection('requirement-stale', 'The decision is not bound to the current normalized task requirement.');
347
478
  }
348
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
+ }
349
483
  if (!candidateSetAuthority
484
+ || isSuperseded(candidateSetAuthority)
350
485
  || input.decision.candidateSetDigest !== input.candidateSet.candidateSetDigest) {
351
486
  return rejection('candidate-set-stale', 'The decision is not bound to the current authentic candidate set.');
352
487
  }
@@ -355,11 +490,10 @@ export function admitAIAgentDefinitionSelection(
355
490
  const candidate = input.candidateSet.candidates.find(({ agentDefinitionRef }) =>
356
491
  agentDefinitionRef === decision.candidateRef);
357
492
  if (!candidate) return rejection('candidate-missing', 'The selected candidate is absent from the current set.');
358
- if (pendingAuthoringAdmissions.has(candidate.agentDefinitionRef)
359
- && candidate.authoringEvidence === undefined) {
493
+ if (pendingFor(candidateSetAuthority)?.has(candidate.agentDefinitionRef)) {
360
494
  return rejection(
361
495
  'authoring-in-progress',
362
- '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.',
363
497
  );
364
498
  }
365
499
  if (candidate.candidateDigest !== decision.candidateDigest) {
@@ -379,6 +513,47 @@ export function admitAIAgentDefinitionSelection(
379
513
  selectedAdmissionAuthorities.set(admission, candidateSetAuthority);
380
514
  return admission;
381
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
+ }
382
557
  if (!isValidAuthoringProposal(input.decision.proposal, input.decision.agentDefinitionRef, input.candidateSet)) {
383
558
  return rejection('authoring-proposal-invalid', 'The author-new decision is not one exact Halfcode create proposal for the current registry.');
384
559
  }
@@ -392,7 +567,7 @@ export function admitAIAgentDefinitionSelection(
392
567
  proposal,
393
568
  reason: stringValue(input.decision.reason, 'decision.reason'),
394
569
  }) satisfies AuthorNewAIAgentDefinitionAdmission;
395
- if (pendingAuthoringAdmissions.has(admission.agentDefinitionRef)) {
570
+ if (pending.has(admission.agentDefinitionRef)) {
396
571
  return rejection('authoring-in-progress', 'An authoring request for this exact Agent definition is already in flight.');
397
572
  }
398
573
  authoringAdmissionAuthorities.set(admission, Object.freeze({
@@ -401,7 +576,7 @@ export function admitAIAgentDefinitionSelection(
401
576
  requirement: input.requirement,
402
577
  proposalDigest: digest(proposal),
403
578
  }));
404
- pendingAuthoringAdmissions.set(admission.agentDefinitionRef, admission);
579
+ pending.set(admission.agentDefinitionRef, admission);
405
580
  return admission;
406
581
  }
407
582
 
@@ -410,8 +585,12 @@ export function freezeSelectedAIAgentTaskBinding(
410
585
  ): FrozenAIAgentTaskBinding {
411
586
  const authority = selectedAdmissionAuthorities.get(input.admission as object);
412
587
  if (!authority
588
+ || !ownerMatches(authority, input.selectionOwner)
589
+ || isSuperseded(authority)
590
+ || pendingFor(authority)?.has(input.admission.candidate.agentDefinitionRef)
413
591
  || authority.registry !== input.registry
414
592
  || authority.projection !== input.projection
593
+ || authority.executionResources !== input.executionResources
415
594
  || input.contentIdentities.get(resourceId(input.admission.candidate.agentDefinitionRef))?.contentDigest
416
595
  !== input.admission.candidate.contentDigest) {
417
596
  throw new AIAgentDefinitionSelectionError(
@@ -420,6 +599,8 @@ export function freezeSelectedAIAgentTaskBinding(
420
599
  );
421
600
  }
422
601
  return projectFrozenAIAgentTaskBinding(freezeAIWorkflowRunResources({
602
+ executionResources: input.executionResources,
603
+ codeExecutions: input.codeExecutions,
423
604
  registry: input.registry,
424
605
  projection: input.projection,
425
606
  task: {
@@ -465,6 +646,20 @@ function isValidDecision(value: AIAgentDefinitionSelectionDecision): boolean {
465
646
  exactRef(decision.agentDefinitionRef, 'decision.agentDefinitionRef');
466
647
  return true;
467
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
+ }
468
663
  return false;
469
664
  } catch {
470
665
  return false;
@@ -472,6 +667,7 @@ function isValidDecision(value: AIAgentDefinitionSelectionDecision): boolean {
472
667
  }
473
668
 
474
669
  function assertProjectionAuthority(input: ProjectAIAgentDefinitionCandidatesInput): void {
670
+ if (input.executionResources) assertAIAgentExecutionResources(input.executionResources, input.registry, input.layers);
475
671
  if (sourceRegistryForAIWorkflowAgentResourceProjection(input.projection) !== input.registry) {
476
672
  throw new AIAgentDefinitionSelectionError(
477
673
  'AI_AGENT_CANDIDATE_PROJECTION_UNTRUSTED',
@@ -494,10 +690,49 @@ function isCompatible(
494
690
  && containsAll(candidate.messageSourceRefs, requirement.requiredMessageSourceRefs);
495
691
  }
496
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
+
497
731
  function isValidAuthoringProposal(
498
732
  value: ResourceAuthoringProposal,
499
733
  definitionRef: `resource://${string}`,
500
734
  candidateSet: AIAgentDefinitionCandidateSet,
735
+ authorityDigestBefore?: Sha256Digest,
501
736
  ): boolean {
502
737
  try {
503
738
  const proposal = closedRecord(value, 'proposal');
@@ -515,15 +750,19 @@ function isValidAuthoringProposal(
515
750
  'expected',
516
751
  ], 'proposal');
517
752
  const expected = closedRecord(proposal.expected, 'proposal.expected');
518
- exactKeys(expected, ['state', 'registryRevision'], 'proposal.expected');
753
+ exactKeys(expected, authorityDigestBefore === undefined
754
+ ? ['state', 'registryRevision'] : ['state', 'registryRevision', 'authorityDigest'], 'proposal.expected');
519
755
  return proposal.schemaVersion === RESOURCE_AUTHORING_SCHEMA_VERSION
520
- && proposal.operation === 'create'
756
+ && proposal.operation === (authorityDigestBefore === undefined ? 'create' : 'update')
521
757
  && proposal.kind === 'AIAgentDefinition'
522
758
  && proposal.sourceShape === 'single-file'
523
- && expected.state === 'absent'
759
+ && expected.state === (authorityDigestBefore === undefined ? 'absent' : 'present')
760
+ && (authorityDigestBefore === undefined || (expected.authorityDigest === authorityDigestBefore
761
+ && sha256Digest(stringValue(proposal.authorityText, 'proposal.authorityText')) !== authorityDigestBefore))
524
762
  && expected.registryRevision === candidateSet.registryRevision
525
763
  && definitionRef === resourceRef(nonEmptyString(proposal.resourceId, 'proposal.resourceId'))
526
- && !candidateSet.candidates.some(({ agentDefinitionRef }) => agentDefinitionRef === definitionRef)
764
+ && (authorityDigestBefore !== undefined
765
+ || !candidateSet.candidates.some(({ agentDefinitionRef }) => agentDefinitionRef === definitionRef))
527
766
  && nonEmptyString(proposal.catalogId, 'proposal.catalogId').length > 0
528
767
  && proposal.envelopeVersion === RESOURCE_ENVELOPE_VERSION
529
768
  && Number.isSafeInteger(proposal.writerSpecVersion)
@@ -0,0 +1,160 @@
1
+ import {
2
+ DEPA_AI_RESOURCE_KIND_READER_REGISTRATIONS,
3
+ type AIAgentCodeResourceProjection,
4
+ type AIAgentResolvedResourceIdentity,
5
+ type AIWorkflowAgentResourceProjection,
6
+ } from 'ai-workflow-contract';
7
+ import {
8
+ composeLayeredResourceRegistry,
9
+ digestCanonical,
10
+ resolveEffectiveResourceContentIdentities,
11
+ type EffectiveResourceRegistry,
12
+ type ResourceContentIdentity,
13
+ type ResourceLayerContentIdentityInput,
14
+ type ResourceRecord,
15
+ type ResolvedResourceRecord,
16
+ type ResourceTombstone,
17
+ } from 'halfcode-compiler.xnl/resource-core';
18
+ import { projectAIWorkflowAgentResources } from './agent-resources.js';
19
+ import { resolveAIWorkflowResourceTree, type AIWorkflowResourceResolutionContext } from './resource-resolution.js';
20
+
21
+ export interface ResolveAIAgentExecutionResourcesRuntime {
22
+ readonly resolutionContexts: ReadonlyMap<string, AIWorkflowResourceResolutionContext>;
23
+ }
24
+ export interface ResolveAIAgentExecutionResourcesInput {
25
+ readonly registry: EffectiveResourceRegistry;
26
+ readonly layers: readonly ResourceLayerContentIdentityInput[];
27
+ }
28
+ declare const executionResourceBrand: unique symbol;
29
+ /** Process-local derived observation; rebuild from authored bytes and exact readers on recovery. */
30
+ export interface AIAgentExecutionResourceObservation {
31
+ readonly [executionResourceBrand]: true;
32
+ readonly registry: EffectiveResourceRegistry;
33
+ readonly projection: AIWorkflowAgentResourceProjection;
34
+ readonly contentIdentities: ReadonlyMap<string, ResourceContentIdentity>;
35
+ readonly resolutionIdentities: ReadonlyMap<string, AIAgentResolvedResourceIdentity>;
36
+ /** Exact native effective records. Custom readerValue semantics remain owned by that Kind. */
37
+ readonly resolvedResources: ReadonlyMap<string, ResolvedResourceRecord>;
38
+ }
39
+ interface ExecutionAuthority extends ResolveAIAgentExecutionResourcesInput {
40
+ readonly layerBindings: readonly ResourceLayerContentIdentityInput[];
41
+ }
42
+ const authorities = new WeakMap<AIAgentExecutionResourceObservation, ExecutionAuthority>();
43
+ const codeResolutions = new WeakMap<AIAgentCodeResourceProjection, AIAgentResolvedResourceIdentity>();
44
+ const executionRegistries = new WeakSet<EffectiveResourceRegistry>();
45
+ const executionCodeResources = new WeakSet<ResourceRecord>();
46
+ const nodeReaderSubjects = new Set(DEPA_AI_RESOURCE_KIND_READER_REGISTRATIONS.map(reader => reader.subjectFqn));
47
+
48
+ export function resolveAIAgentExecutionResources(
49
+ runtime: ResolveAIAgentExecutionResourcesRuntime,
50
+ input: ResolveAIAgentExecutionResourcesInput,
51
+ ): AIAgentExecutionResourceObservation {
52
+ // Native owner validates registry provenance, exact authored trees, layer order and origins.
53
+ const contentIdentities = resolveEffectiveResourceContentIdentities(input);
54
+ if (runtime.resolutionContexts.size !== input.layers.length
55
+ || input.layers.some(layer => !runtime.resolutionContexts.get(layer.id))) {
56
+ fail('Reader context keys must exactly match the authored layer ids.');
57
+ }
58
+ // Tombstones are facts of the authentic composer result. Reuse the native composer
59
+ // to replay them, including tombstones that were subsequently shadowed by a layer.
60
+ const tombstones = new Map<string, ResourceTombstone[]>();
61
+ for (const entry of input.registry.byId.values()) for (const tombstone of entry.tombstones) {
62
+ const values = tombstones.get(tombstone.layerId) ?? [];
63
+ values.push({ resourceId: tombstone.resourceId,
64
+ ...(tombstone.expectedKind === undefined ? {} : { expectedKind: tombstone.expectedKind }),
65
+ ...(tombstone.reason === undefined ? {} : { reason: tombstone.reason }) });
66
+ tombstones.set(tombstone.layerId, values);
67
+ }
68
+ const resolvedLayers = input.layers.map(layer => ({
69
+ id: layer.id,
70
+ tree: resolveAIWorkflowResourceTree(layer.tree, runtime.resolutionContexts.get(layer.id)!),
71
+ tombstones: tombstones.get(layer.id) ?? [],
72
+ }));
73
+ const registry = composeLayeredResourceRegistry({ layers: resolvedLayers });
74
+ if (digestCanonical(topology(registry)) !== digestCanonical(topology(input.registry))) {
75
+ fail('Resolved composition must retain the exact authored layer, origin, shadow and tombstone topology.');
76
+ }
77
+ const resolutions: Array<readonly [string, AIAgentResolvedResourceIdentity]> = [];
78
+ const resolvedResources: Array<readonly [string, ResolvedResourceRecord]> = [];
79
+ for (const entry of registry.byId.values()) {
80
+ if (!entry.resource) continue;
81
+ const layer = resolvedLayers.find(value => value.id === entry.effectiveLayerId);
82
+ const record = layer?.tree.registry.byKind.get(entry.kind)?.find(value => value.resourceId === entry.resourceId);
83
+ if (!record || !layer) fail(`Missing native resolution for '${entry.resourceId}'.`);
84
+ resolvedResources.push([entry.resourceId, record]);
85
+ // This projector's explicit input protocol is resolved PortableSpec/node. It does
86
+ // not execute arbitrary Kind reader outputs with different business semantics.
87
+ if (nodeReaderSubjects.has(record.subjectFqn)
88
+ && digestCanonical(record.readerValue) !== digestCanonical(record.resolvedSpec)) {
89
+ fail(`Kind '${record.kind}' requires a reader whose value preserves the resolved node input protocol.`);
90
+ }
91
+ resolutions.push([entry.resourceId, Object.freeze({
92
+ resourceId: record.resourceId, readerId: record.readerId, readerSpecVersion: record.readerSpecVersion,
93
+ effectiveContentDigest: record.effectiveContentDigest, readerProfileId: layer.tree.readerProfileId,
94
+ resolutionReceiptDigest: digestCanonical(record.resolution),
95
+ })]);
96
+ }
97
+ const resolutionIdentities = immutableMap(resolutions.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
98
+ const projection = projectAIWorkflowAgentResources(registry);
99
+ for (const code of projection.codeResources ?? []) {
100
+ codeResolutions.set(code, resolutionIdentities.get(code.resource.resourceId)!);
101
+ executionCodeResources.add(code.resource);
102
+ }
103
+ const observation = Object.freeze({ registry, projection, contentIdentities, resolutionIdentities,
104
+ resolvedResources: immutableMap(resolvedResources.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)),
105
+ }) as AIAgentExecutionResourceObservation;
106
+ authorities.set(observation, Object.freeze({ registry: input.registry, layers: input.layers,
107
+ layerBindings: Object.freeze(input.layers.map(layer => Object.freeze({ id: layer.id, tree: layer.tree }))) }));
108
+ executionRegistries.add(registry);
109
+ return observation;
110
+ }
111
+
112
+ /** Internal admission guard; never exported as a public branding operation. */
113
+ export function assertAIAgentExecutionResources(
114
+ observation: AIAgentExecutionResourceObservation,
115
+ registry: EffectiveResourceRegistry,
116
+ layers?: readonly ResourceLayerContentIdentityInput[],
117
+ ): AIAgentExecutionResourceObservation {
118
+ const authority = authorities.get(observation);
119
+ if (!authority || authority.registry !== registry || (layers !== undefined && authority.layers !== layers)
120
+ || authority.layers.length !== authority.layerBindings.length
121
+ || authority.layerBindings.some((layer, index) => authority.layers[index]?.id !== layer.id || authority.layers[index]?.tree !== layer.tree)) {
122
+ fail('Execution resources require the exact authentic observation for this authored registry and layers.');
123
+ }
124
+ return observation;
125
+ }
126
+
127
+ export function resolutionForAIAgentCodeProjection(projection: AIAgentCodeResourceProjection): AIAgentResolvedResourceIdentity | undefined {
128
+ const resolution = codeResolutions.get(projection);
129
+ if (!resolution && executionCodeResources.has(projection.resource)) {
130
+ fail('Code compilation and restore require the exact execution observation projection.');
131
+ }
132
+ return resolution;
133
+ }
134
+
135
+ /** Prevents discarding reader identities by passing a bridge registry to the old freeze path. */
136
+ export function assertAIAgentFreezeRegistry(registry: EffectiveResourceRegistry): void {
137
+ if (executionRegistries.has(registry)) fail('Freeze requires the original authored registry and its executionResources observation.');
138
+ }
139
+
140
+ function topology(registry: EffectiveResourceRegistry): unknown {
141
+ return { layers: registry.layers, entries: [...registry.byId.values()].map(entry => ({
142
+ resourceId: entry.resourceId, kind: entry.kind, present: entry.resource !== undefined,
143
+ origin: entry.effectiveOrigin ?? null, shadowed: entry.shadowed,
144
+ tombstone: entry.tombstone ?? null, tombstones: entry.tombstones,
145
+ })) };
146
+ }
147
+ function immutableMap<K, V>(entries: readonly (readonly [K, V])[]): ReadonlyMap<K, V> {
148
+ const values = new Map(entries);
149
+ const view: ReadonlyMap<K, V> = Object.freeze({
150
+ get size() { return values.size; },
151
+ get: (key: K) => values.get(key), has: (key: K) => values.has(key),
152
+ keys: () => values.keys(), values: () => values.values(), entries: () => values.entries(),
153
+ [Symbol.iterator]: () => values[Symbol.iterator](),
154
+ forEach: (callback: (value: V, key: K, map: ReadonlyMap<K, V>) => void, thisArg?: unknown) => {
155
+ values.forEach((value, key) => callback.call(thisArg, value, key, view));
156
+ },
157
+ });
158
+ return view;
159
+ }
160
+ function fail(message: string): never { throw new Error(`AI_AGENT_EXECUTION_RESOURCES_UNTRUSTED: ${message}`); }